Harden canonical publication against races
This commit is contained in:
parent
8919e2af32
commit
a901c9705b
8 changed files with 1059 additions and 78 deletions
|
|
@ -2,15 +2,77 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import errno
|
||||
import os
|
||||
import secrets
|
||||
import stat
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Protocol, cast
|
||||
|
||||
from .errors import DocForgeError
|
||||
|
||||
RENAME_EXCHANGE = 2
|
||||
|
||||
|
||||
class _RenameAt2(Protocol):
|
||||
argtypes: list[object]
|
||||
restype: object
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
old_directory_fd: int,
|
||||
old_name: bytes,
|
||||
new_directory_fd: int,
|
||||
new_name: bytes,
|
||||
flags: int,
|
||||
/,
|
||||
) -> int: ...
|
||||
|
||||
|
||||
def rename_exchange_at(directory_fd: int, first: str, second: str) -> None:
|
||||
"""Atomically exchange two names inside one already bound directory."""
|
||||
|
||||
library = ctypes.CDLL(None, use_errno=True)
|
||||
try:
|
||||
rename_at2 = cast(_RenameAt2, library.renameat2)
|
||||
except AttributeError as error:
|
||||
raise DocForgeError(
|
||||
"atomic_exchange_unavailable",
|
||||
"Atomic exchange is unavailable on this platform",
|
||||
) from error
|
||||
rename_at2.argtypes = [
|
||||
ctypes.c_int,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_uint,
|
||||
]
|
||||
rename_at2.restype = ctypes.c_int
|
||||
ctypes.set_errno(0)
|
||||
result = rename_at2(
|
||||
directory_fd,
|
||||
os.fsencode(first),
|
||||
directory_fd,
|
||||
os.fsencode(second),
|
||||
RENAME_EXCHANGE,
|
||||
)
|
||||
if result == 0:
|
||||
return
|
||||
error_number = ctypes.get_errno()
|
||||
if error_number in {errno.ENOSYS, errno.EINVAL, errno.EOPNOTSUPP}:
|
||||
raise DocForgeError(
|
||||
"atomic_exchange_unavailable",
|
||||
"Atomic exchange is unavailable on this filesystem",
|
||||
)
|
||||
raise DocForgeError(
|
||||
"publication_failure",
|
||||
"Could not exchange atomic publication paths",
|
||||
error_number=error_number,
|
||||
) from OSError(error_number, os.strerror(error_number))
|
||||
|
||||
|
||||
def open_bound_directory(path: Path) -> int:
|
||||
"""Open one real directory and bind its current inode for later operations."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue