From 0e3a2433817c7eb195296786d5726f03f118cc4b Mon Sep 17 00:00:00 2001 From: David Vitek Date: Sun, 10 Apr 2022 20:55:22 +0000 Subject: [PATCH] Fix race conditions in FileSystemBytecodeCache --- CHANGES.rst | 1 + src/jinja2/bccache.py | 55 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index da7d0a682..96a9567cb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ Unreleased - Add parameters to ``Environment.overlay`` to match ``__init__``. :issue:`1645` +- Handle race condition in ``FileSystemBytecodeCache``. :issue:`1654` Version 3.1.1 diff --git a/src/jinja2/bccache.py b/src/jinja2/bccache.py index 3bb61b7c3..368cc898a 100644 --- a/src/jinja2/bccache.py +++ b/src/jinja2/bccache.py @@ -7,6 +7,7 @@ """ import errno import fnmatch +import logging import marshal import os import pickle @@ -79,7 +80,7 @@ def load_bytecode(self, f: t.BinaryIO) -> None: self.reset() return - def write_bytecode(self, f: t.BinaryIO) -> None: + def write_bytecode(self, f: t.IO[bytes]) -> None: """Dump the bytecode into the file or file like object passed.""" if self.code is None: raise TypeError("can't write empty bucket") @@ -262,13 +263,55 @@ def _get_cache_filename(self, bucket: Bucket) -> str: def load_bytecode(self, bucket: Bucket) -> None: filename = self._get_cache_filename(bucket) - if os.path.exists(filename): - with open(filename, "rb") as f: - bucket.load_bytecode(f) + # Don't test for existence before opening the file, since the + # file could disappear after the test before the open. + try: + f = open(filename, "rb") + except (FileNotFoundError, IsADirectoryError, PermissionError): + # PermissionError can occur on Windows when an operation is + # in progress, such as calling clear(). + return + + with f: + bucket.load_bytecode(f) def dump_bytecode(self, bucket: Bucket) -> None: - with open(self._get_cache_filename(bucket), "wb") as f: - bucket.write_bytecode(f) + # Write to a temporary file, then rename to the real name after + # writing. This avoids another process reading the file before + # it is fully written. + name = self._get_cache_filename(bucket) + f = tempfile.NamedTemporaryFile( + mode="wb", + dir=os.path.dirname(name), + prefix=os.path.basename(name), + suffix=".tmp", + delete=False, + ) + + def remove_silent() -> None: + try: + os.remove(f.name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + pass + + try: + with f: + bucket.write_bytecode(f) + except BaseException: + remove_silent() + raise + + try: + os.replace(f.name, name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + remove_silent() + except BaseException: + remove_silent() + raise def clear(self) -> None: # imported lazily here because google app-engine doesn't support