Skip to content

Commit 2258629

Browse files
committed
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: #2836 Link: https://stackoverflow.com/questions/489861/locking-a-file-in-python Signed-off-by: Stefan Berger <stefanb@linux.ibm.com>
1 parent a01210b commit 2258629

File tree

2 files changed

+29
-3
lines changed

2 files changed

+29
-3
lines changed

tuf/ngclient/file_lock.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2025, New York University and the TUF contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
"""Minimal file locking implementation.
5+
6+
Source: https://stackoverflow.com/questions/489861/locking-a-file-in-python
7+
"""
8+
9+
try:
10+
# Posix based file locking (Linux, Ubuntu, MacOS, etc.)
11+
# Only allows locking on writable files, might cause
12+
# strange results for reading.
13+
import fcntl
14+
def lock_file(f):
15+
if f.writable(): fcntl.lockf(f, fcntl.LOCK_EX)
16+
except ModuleNotFoundError:
17+
# Windows file locking
18+
import msvcrt
19+
def file_size(f):
20+
return os.path.getsize( os.path.realpath(f.name) )
21+
def lock_file(f):
22+
msvcrt.locking(f.fileno(), msvcrt.LK_RLCK, file_size(f))

tuf/ngclient/updater.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from tuf.api.metadata import Root, Snapshot, TargetFile, Targets, Timestamp
6767
from tuf.ngclient._internal.trusted_metadata_set import TrustedMetadataSet
6868
from tuf.ngclient.config import EnvelopeType, UpdaterConfig
69+
from tuf.ngclient.file_lock import lock_file
6970
from tuf.ngclient.urllib3_fetcher import Urllib3Fetcher
7071

7172
if TYPE_CHECKING:
@@ -362,9 +363,12 @@ def _update_root_symlink(self) -> None:
362363
linkname = os.path.join(self._dir, "root.json")
363364
version = self._trusted_set.root.version
364365
current = os.path.join("root_history", f"{version}.root.json")
365-
with contextlib.suppress(FileNotFoundError):
366-
os.remove(linkname)
367-
os.symlink(current, linkname)
366+
367+
with open(os.path.join(self._dir, current + ".lck"), "wb") as f:
368+
lock_file(f)
369+
with contextlib.suppress(FileNotFoundError):
370+
os.remove(linkname)
371+
os.symlink(current, linkname)
368372

369373
def _load_root(self) -> None:
370374
"""Load root metadata.

0 commit comments

Comments
 (0)