flat_hash: Fix race condition in _setitem.

When setting a value in a flat_hash cache, the pid is used to create a
unique temporary file. If two different processes with the same pid
(different pid namespaces) try to set the cache, the code is racy.

This change converts _setitem to use tempfile which does not rely on
the pid being unique.

BUG=chromium:477727
TEST=trybot run on chromiumos-sdk.

Change-Id: Idee01df326e0408778ad84d7721350c7d7c03ad8
Reviewed-on: https://chromium-review.googlesource.com/270802
Reviewed-by: David James <davidjames@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Tested-by: Bertrand Simonnet <bsimonnet@chromium.org>
diff --git a/pym/portage/cache/flat_hash.py b/pym/portage/cache/flat_hash.py
index 5304296..4735c86 100644
--- a/pym/portage/cache/flat_hash.py
+++ b/pym/portage/cache/flat_hash.py
@@ -10,6 +10,7 @@
 import io
 import stat
 import sys
+import tempfile
 import os as _os
 from portage import os
 from portage import _encodings
@@ -66,48 +67,39 @@
 			raise cache_errors.CacheCorruption(cpv, e)
 
 	def _setitem(self, cpv, values):
-		s = cpv.rfind("/")
-		fp = os.path.join(self.location,cpv[:s],".update.%i.%s" % (os.getpid(), cpv[s+1:]))
-		try:
-			myf = io.open(_unicode_encode(fp,
-				encoding=_encodings['fs'], errors='strict'),
-				mode='w', encoding=_encodings['repo.content'],
-				errors='backslashreplace')
-		except (IOError, OSError) as e:
-			if errno.ENOENT == e.errno:
+		with tempfile.NamedTemporaryFile(delete=False, dir=self.location,
+			prefix=cpv.replace('/', '_')) as temp:
+			temp.close()
+			try:
+				with io.open(temp.name, mode='w',
+					encoding=_encodings['repo.content'],
+					errors='backslashreplace') as myf:
+					for k in self._write_keys:
+						v = values.get(k)
+						if not v:
+							continue
+						# NOTE: This format string requires unicode_literals, so that
+						# k and v are coerced to unicode, in order to prevent TypeError
+						# when writing raw bytes to TextIOWrapper with Python 2.
+						myf.write("%s=%s\n" % (k, v))
+
+				self._ensure_access(temp.name)
+
+				# Update written, we can move it.
+				new_fp = os.path.join(self.location, cpv)
 				try:
-					self._ensure_dirs(cpv)
-					myf = io.open(_unicode_encode(fp,
-						encoding=_encodings['fs'], errors='strict'),
-						mode='w', encoding=_encodings['repo.content'],
-						errors='backslashreplace')
-				except (OSError, IOError) as e:
-					raise cache_errors.CacheCorruption(cpv, e)
-			else:
+					os.rename(temp.name, new_fp)
+				except OSError as e:
+					if e.errno == errno.ENOENT:
+						self._ensure_dirs(cpv)
+						os.rename(temp.name, new_fp)
+					else:
+						raise cache_errors.CacheCorruption(cpv, e)
+
+			except EnvironmentError as e:
+				os.remove(temp.name)
 				raise cache_errors.CacheCorruption(cpv, e)
 
-		try:
-			for k in self._write_keys:
-				v = values.get(k)
-				if not v:
-					continue
-				# NOTE: This format string requires unicode_literals, so that
-				# k and v are coerced to unicode, in order to prevent TypeError
-				# when writing raw bytes to TextIOWrapper with Python 2.
-				myf.write("%s=%s\n" % (k, v))
-		finally:
-			myf.close()
-		self._ensure_access(fp)
-
-		#update written.  now we move it.
-
-		new_fp = os.path.join(self.location,cpv)
-		try:
-			os.rename(fp, new_fp)
-		except (OSError, IOError) as e:
-			os.remove(fp)
-			raise cache_errors.CacheCorruption(cpv, e)
-
 	def _delitem(self, cpv):
 #		import pdb;pdb.set_trace()
 		try: