Skip to content

Commit 1823a9e

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 <[email protected]>
1 parent a01210b commit 1823a9e

File tree

2 files changed

+46
-6
lines changed

2 files changed

+46
-6
lines changed

tuf/ngclient/_internal/file_lock.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2025, New York University and the TUF contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
"""Platform-specific support for file locking
5+
6+
"""
7+
8+
import os
9+
import sys
10+
11+
12+
if sys.platform in ['win32']:
13+
def create_lockfile(filename: str) -> str:
14+
# Use the name of the file but create lockfile in %TEMP%.
15+
fn = os.path.basename(filename)
16+
lockfile = os.path.join(os.getenv("TEMP"), "tuf" + fn)
17+
#try:
18+
# with open(filename, "w+") as f:
19+
# f.truncate(1)
20+
# f.flush()
21+
#except:
22+
# pass
23+
return lockfile
24+
else:
25+
def create_lockfile(filename: str) -> str:
26+
return filename

tuf/ngclient/updater.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@
6262
from typing import TYPE_CHECKING, cast
6363
from urllib import parse
6464

65+
from filelock import FileLock
66+
6567
from tuf.api import exceptions
6668
from tuf.api.metadata import Root, Snapshot, TargetFile, Targets, Timestamp
69+
from tuf.ngclient._internal.file_lock import create_lockfile
6770
from tuf.ngclient._internal.trusted_metadata_set import TrustedMetadataSet
6871
from tuf.ngclient.config import EnvelopeType, UpdaterConfig
6972
from tuf.ngclient.urllib3_fetcher import Urllib3Fetcher
@@ -348,7 +351,11 @@ def _persist_file(self, filename: str, data: bytes) -> None:
348351
) as temp_file:
349352
temp_file_name = temp_file.name
350353
temp_file.write(data)
351-
os.replace(temp_file.name, filename)
354+
#print(f"filename: {filename}")
355+
356+
lck = os.path.join(self._dir, ".root.json.lck")
357+
with FileLock(create_lockfile(lck)):
358+
os.replace(temp_file.name, filename)
352359
except OSError as e:
353360
# remove tempfile if we managed to create one,
354361
# then let the exception happen
@@ -362,9 +369,13 @@ def _update_root_symlink(self) -> None:
362369
linkname = os.path.join(self._dir, "root.json")
363370
version = self._trusted_set.root.version
364371
current = os.path.join("root_history", f"{version}.root.json")
365-
with contextlib.suppress(FileNotFoundError):
366-
os.remove(linkname)
367-
os.symlink(current, linkname)
372+
373+
#print(f"lck:{lck}")
374+
lck = os.path.join(self._dir, ".root.json.lck")
375+
with FileLock(create_lockfile(lck)):
376+
with contextlib.suppress(FileNotFoundError):
377+
os.remove(linkname)
378+
os.symlink(current, linkname)
368379

369380
def _load_root(self) -> None:
370381
"""Load root metadata.
@@ -375,6 +386,7 @@ def _load_root(self) -> None:
375386
If metadata is loaded from remote repository, store it in local cache.
376387
"""
377388

389+
Path(self._dir).mkdir(exist_ok=True, parents=True)
378390
# Update the root role
379391
lower_bound = self._trusted_set.root.version + 1
380392
upper_bound = lower_bound + self.config.max_root_rotations
@@ -386,8 +398,10 @@ def _load_root(self) -> None:
386398
root_path = os.path.join(
387399
self._dir, "root_history", f"{next_version}.root.json"
388400
)
389-
with open(root_path, "rb") as f:
390-
self._trusted_set.update_root(f.read())
401+
lck = os.path.join(self._dir, ".root.json.lck")
402+
with FileLock(create_lockfile(lck)):
403+
with open(root_path, "rb") as f:
404+
self._trusted_set.update_root(f.read())
391405
continue
392406
except (OSError, exceptions.RepositoryError) as e:
393407
# this root did not exist locally or is invalid

0 commit comments

Comments
 (0)