From 2258629d43113a8088ba70966bd77c92be1c68d3 Mon Sep 17 00:00:00 2001 From: Stefan Berger Date: Tue, 15 Jul 2025 12:04:52 -0500 Subject: [PATCH] Use a lock file to avoid exceptions due to concurrenct symlink creation We have seen exceptions being raised from _update_root_symlink() on the level of the sigstore-python library when multiple concurrent threads were creating symlinks in this function with the same symlink name (in a test environment running tests concurrently). To avoid this issue, have each thread open a lock file and create an exclusive lock on it to serialize the access to the removal and creation of the symlink. The reproducer for this issue, that should be run in 2 or more python interpreters concurrently, looks like this: from sigstore import sign while True: sign.TrustedRoot.production() Use fcntl.lockf-based locking for Linux and Mac and a different implementation on Windows. The source originally comes from a discussion on stockoverflow (link below). Resolves: https://github.com/theupdateframework/python-tuf/issues/2836 Link: https://stackoverflow.com/questions/489861/locking-a-file-in-python Signed-off-by: Stefan Berger --- tuf/ngclient/file_lock.py | 22 ++++++++++++++++++++++ tuf/ngclient/updater.py | 10 +++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 tuf/ngclient/file_lock.py diff --git a/tuf/ngclient/file_lock.py b/tuf/ngclient/file_lock.py new file mode 100644 index 0000000000..c5cb3da5b6 --- /dev/null +++ b/tuf/ngclient/file_lock.py @@ -0,0 +1,22 @@ +# Copyright 2025, New York University and the TUF contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + +"""Minimal file locking implementation. + +Source: https://stackoverflow.com/questions/489861/locking-a-file-in-python +""" + +try: + # Posix based file locking (Linux, Ubuntu, MacOS, etc.) + # Only allows locking on writable files, might cause + # strange results for reading. + import fcntl + def lock_file(f): + if f.writable(): fcntl.lockf(f, fcntl.LOCK_EX) +except ModuleNotFoundError: + # Windows file locking + import msvcrt + def file_size(f): + return os.path.getsize( os.path.realpath(f.name) ) + def lock_file(f): + msvcrt.locking(f.fileno(), msvcrt.LK_RLCK, file_size(f)) diff --git a/tuf/ngclient/updater.py b/tuf/ngclient/updater.py index a98e799ce4..5431a1cec5 100644 --- a/tuf/ngclient/updater.py +++ b/tuf/ngclient/updater.py @@ -66,6 +66,7 @@ from tuf.api.metadata import Root, Snapshot, TargetFile, Targets, Timestamp from tuf.ngclient._internal.trusted_metadata_set import TrustedMetadataSet from tuf.ngclient.config import EnvelopeType, UpdaterConfig +from tuf.ngclient.file_lock import lock_file from tuf.ngclient.urllib3_fetcher import Urllib3Fetcher if TYPE_CHECKING: @@ -362,9 +363,12 @@ def _update_root_symlink(self) -> None: linkname = os.path.join(self._dir, "root.json") version = self._trusted_set.root.version current = os.path.join("root_history", f"{version}.root.json") - with contextlib.suppress(FileNotFoundError): - os.remove(linkname) - os.symlink(current, linkname) + + with open(os.path.join(self._dir, current + ".lck"), "wb") as f: + lock_file(f) + with contextlib.suppress(FileNotFoundError): + os.remove(linkname) + os.symlink(current, linkname) def _load_root(self) -> None: """Load root metadata.