124 lines
5.1 KiB
Python
124 lines
5.1 KiB
Python
"""Fail-closed verifier for the backend-owned writer capability.
|
|
|
|
FD 3 is the inherited writer open file description and FD 4 is the retained
|
|
workspace-root directory. Environment values are descriptive identity only;
|
|
they never authorize a direct invocation.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import errno
|
|
import fcntl
|
|
import os
|
|
import re
|
|
import stat
|
|
import struct
|
|
import sys
|
|
from dataclasses import dataclass
|
|
|
|
|
|
class WorkspaceWriterConflict(RuntimeError):
|
|
"preprocessing_conflict"
|
|
|
|
def __init__(self, message: str = "preprocessing_conflict") -> None:
|
|
super().__init__(message)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WorkspaceCapability:
|
|
workspace_id: str
|
|
revision: str
|
|
device: int
|
|
inode: int
|
|
writer_device: int
|
|
writer_inode: int
|
|
|
|
|
|
def _identity(env: dict[str, str]) -> tuple[str, str, int, int]:
|
|
wid, rev = env.get("THOTH_WORKSPACE_ID"), env.get("THOTH_WORKSPACE_REVISION")
|
|
if not wid or not rev or not re.fullmatch(r"[a-z][a-z0-9-]{2,62}", wid) or not re.fullmatch(r"[0-9a-f]{40}", rev):
|
|
raise WorkspaceWriterConflict()
|
|
try:
|
|
device, inode = int(env["THOTH_WORKSPACE_DEVICE"]), int(env["THOTH_WORKSPACE_INODE"])
|
|
except (KeyError, ValueError):
|
|
raise WorkspaceWriterConflict() from None
|
|
if device < 0 or inode <= 0:
|
|
raise WorkspaceWriterConflict()
|
|
return wid, rev, device, inode
|
|
|
|
|
|
def _fstat(fd: int) -> os.stat_result:
|
|
try:
|
|
return os.fstat(fd)
|
|
except OSError:
|
|
raise WorkspaceWriterConflict() from None
|
|
|
|
|
|
def _open_lock(root_fd: int) -> int:
|
|
# The lock is opened relative to the retained root and cannot be substituted
|
|
# by a symlink between validation and open. No path fallback is permitted.
|
|
try:
|
|
return os.open("writer.lock", os.O_RDWR | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=root_fd)
|
|
except OSError:
|
|
raise WorkspaceWriterConflict() from None
|
|
|
|
|
|
def verify_workspace_writer_fds(*, writer_fd: int = 3, root_fd: int = 4, env: dict[str, str] | None = None) -> WorkspaceCapability:
|
|
env = dict(os.environ if env is None else env)
|
|
wid, rev, device, inode = _identity(env)
|
|
if writer_fd == root_fd or writer_fd < 0 or root_fd < 0:
|
|
raise WorkspaceWriterConflict()
|
|
root, writer = _fstat(root_fd), _fstat(writer_fd)
|
|
uid = os.getuid()
|
|
if not stat.S_ISDIR(root.st_mode) or root.st_uid != uid or (root.st_mode & 0o777) != 0o700 or (root.st_dev, root.st_ino) != (device, inode):
|
|
raise WorkspaceWriterConflict()
|
|
if not stat.S_ISREG(writer.st_mode) or writer.st_uid != uid or (writer.st_mode & 0o777) != 0o600 or writer.st_nlink != 1:
|
|
raise WorkspaceWriterConflict()
|
|
lock_fd = _open_lock(root_fd)
|
|
try:
|
|
lock = _fstat(lock_fd)
|
|
if (lock.st_dev, lock.st_ino) != (writer.st_dev, writer.st_ino) or not stat.S_ISREG(lock.st_mode) or lock.st_uid != uid or (lock.st_mode & 0o777) != 0o600 or lock.st_nlink != 1:
|
|
raise WorkspaceWriterConflict()
|
|
# Probe using a *different* open file description. An inherited FD 3
|
|
# is valid only when its lock is already held: the independent probe
|
|
# must therefore receive EWOULDBLOCK. We deliberately never flock(3)
|
|
# here: doing so would turn an unheld, independently opened descriptor
|
|
# into an apparently valid capability.
|
|
try:
|
|
# Linux OFD locks identify the open file description rather than
|
|
# the process. The inherited FD 3 must already own this lock;
|
|
# an independent unlocked description can acquire the probe and
|
|
# is rejected. Darwin has no OFD constants, so retain flock's
|
|
# equivalent open-description probe there.
|
|
ofd_setlk = getattr(fcntl, "F_OFD_SETLK", None) if sys.platform.startswith("linux") else None
|
|
if ofd_setlk is not None:
|
|
lock_record = struct.pack("hhqqi", fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0)
|
|
fcntl.fcntl(lock_fd, ofd_setlk, lock_record)
|
|
unlock_record = struct.pack("hhqqi", fcntl.F_UNLCK, os.SEEK_SET, 0, 0, 0)
|
|
fcntl.fcntl(lock_fd, ofd_setlk, unlock_record)
|
|
raise WorkspaceWriterConflict()
|
|
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except OSError as exc:
|
|
if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EWOULDBLOCK):
|
|
raise WorkspaceWriterConflict() from exc
|
|
else:
|
|
try:
|
|
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
except OSError:
|
|
pass
|
|
raise WorkspaceWriterConflict()
|
|
finally:
|
|
try:
|
|
os.close(lock_fd)
|
|
except OSError:
|
|
pass
|
|
return WorkspaceCapability(wid, rev, root.st_dev, root.st_ino, writer.st_dev, writer.st_ino)
|
|
|
|
|
|
def require_workspace_writer_capability(*, workspace_id: str | None = None, revision: str | None = None) -> WorkspaceCapability:
|
|
cap = verify_workspace_writer_fds()
|
|
if workspace_id is not None and cap.workspace_id != workspace_id:
|
|
raise WorkspaceWriterConflict()
|
|
if revision is not None and cap.revision != revision:
|
|
raise WorkspaceWriterConflict()
|
|
return cap
|