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: