Add shared managed lock broker

This commit is contained in:
2026-10-07 02:23:39 +00:00
parent 18676070b2
commit 470c7fb09b
16 changed files with 1583 additions and 120 deletions
@@ -14,6 +14,14 @@ _SPEC_PATH = (Path(__file__).resolve().parents[4] /
_spec = importlib.util.spec_from_file_location('gitea_native_spec_support', _SPEC_PATH)
_support = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_support)
_LOCK_PATH = Path(__file__).resolve().parents[5] / 'lib/managed_lock.py'
_lock_spec = importlib.util.spec_from_file_location('gitea_managed_lock', _LOCK_PATH)
_locks = importlib.util.module_from_spec(_lock_spec)
_lock_spec.loader.exec_module(_locks)
_BROKER_PATH = Path(__file__).resolve().parents[5] / 'lib/managed_lock_broker.py'
_broker_spec = importlib.util.spec_from_file_location('gitea_managed_lock_broker', _BROKER_PATH)
_broker = importlib.util.module_from_spec(_broker_spec)
_broker_spec.loader.exec_module(_broker)
ValidationError = _support.ValidationError
loads_json = _support.loads_json
collect_pages = _support.collect_pages
@@ -646,83 +654,158 @@ class ApiAdapter:
return data
def _validate_registered_source(plan, policy_path, deadline_monotonic):
import math, os, stat, subprocess, time
if type(deadline_monotonic) not in [int, float] or not math.isfinite(deadline_monotonic):
fail('invalid or expired Issue lease')
t = plan['target']
info = _support.validate_cache_paths(policy_path, t['owner'], t['repository'], t['issue'])
wt = Path(info['repository']) / 'worktrees' / f'issue-{t["issue"]}' / 'checkout'
opened = []
def close_opened():
while opened:
try: os.close(opened.pop())
except OSError: pass
def reject(message):
close_opened()
fail(message)
def open_dir(path):
parts = path.parts
fd = os.open(parts[0], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC)
opened.append(fd)
try:
for part in parts[1:]:
nxt = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=fd)
opened.append(nxt); os.close(fd); opened.remove(fd); fd = nxt
return fd
except BaseException:
close_opened(); raise
try:
worktrees_fd = open_dir(wt.parent.parent)
issue_fd = os.open(wt.parent.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=worktrees_fd)
opened.append(issue_fd)
checkout_fd = os.open(wt.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=issue_fd)
opened.append(checkout_fd)
for directory_fd in [worktrees_fd, issue_fd, checkout_fd]:
st = os.fstat(directory_fd)
if (not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid()
or st.st_mode & (stat.S_IWGRP | stat.S_IWOTH)):
reject('unsafe existing worktree path')
state_fd = os.open('state.json', os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=issue_fd)
opened.append(state_fd)
git_pointer_fd = os.open('.git', os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=checkout_fd)
opened.append(git_pointer_fd)
state_st = os.fstat(state_fd); git_st = os.fstat(git_pointer_fd)
if not stat.S_ISREG(state_st.st_mode) or state_st.st_uid != os.getuid() or stat.S_IMODE(state_st.st_mode) != 0o600:
reject('unsafe worktree state')
if not stat.S_ISREG(git_st.st_mode) or git_st.st_uid != os.getuid():
reject('invalid worktree Git pointer')
chunks = []
while chunk := os.read(state_fd, 65536):
chunks.append(chunk)
try:
state = loads_json(b''.join(chunks).decode('utf-8'))
except Exception:
close_opened()
raise
os.lseek(git_pointer_fd, 0, os.SEEK_SET)
pointer = os.read(git_pointer_fd, 4096).decode('utf-8').strip()
if not pointer.startswith('gitdir: '):
reject('invalid worktree Git pointer')
worktree_git_fd = open_dir(Path(pointer[8:]))
except OSError:
close_opened()
raise ValidationError('unsafe existing worktree path') from None
if (type(state) is not dict or state.get('schema') != 'gitea-agent.worktree/v1' or state.get('owner') != t['owner']
or state.get('repository') != t['repository'] or type(state.get('issue')) is not int
or state['issue'] != t['issue'] or state.get('created_by_role') != 'gitea-impl-agent'
or state.get('mode') != 'detached'):
reject('worktree registration identity mismatch')
git_binary = '/usr/bin/git'; binary_stat = Path(git_binary).lstat()
if not stat.S_ISREG(binary_stat.st_mode) or binary_stat.st_uid != 0 or binary_stat.st_mode & 0o022:
reject('untrusted system Git binary')
env = {'PATH': '/usr/bin:/bin', 'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_OPTIONAL_LOCKS': '0'}
git_dir_fd = open_dir(Path(info['repository_git']))
def git(*args):
try:
process = subprocess.run([git_binary, '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null',
'-c', 'safe.directory=/proc/self/fd/' + str(checkout_fd),
'--git-dir=/proc/self/fd/' + str(worktree_git_fd),
'--work-tree=/proc/self/fd/' + str(checkout_fd), *args], capture_output=True, text=True, env=env, timeout=15,
pass_fds=(checkout_fd, worktree_git_fd, git_dir_fd))
except Exception:
close_opened()
raise ValidationError('read-only Git check unavailable') from None
if process.returncode or len(process.stdout) > 1000000:
reject('read-only Git check failed')
return process.stdout
if git('rev-parse', 'HEAD').strip() != t['head']:
reject('worktree HEAD mismatch')
if git('status', '--porcelain=v1', '--untracked-files=all').strip():
reject('existing worktree is not clean')
if git('rev-parse', '--path-format=absolute', '--git-common-dir').strip() != info['repository_git']:
reject('worktree common Git directory mismatch')
records = git('--git-dir=/proc/self/fd/' + str(git_dir_fd), 'worktree', 'list', '--porcelain', '-z').split('\0\0')
matches = [r.split('\0') for r in records if r.startswith('worktree ' + str(wt) + '\0')]
if len(matches) != 1 or 'detached' not in matches[0] or not any(x == 'locked' or x.startswith('locked ') for x in matches[0]):
reject('worktree detached/locked registration mismatch')
close_opened()
if time.monotonic() >= deadline_monotonic:
fail('Issue lease expired during source checks')
class HeldIssueLease:
"""Validate a caller-owned Issue lock FD; never close/release the caller's FD."""
def __init__(self, issue_fd, policy_path, deadline_monotonic):
self.issue_fd = issue_fd
self.policy_path = policy_path
self.deadline_monotonic = deadline_monotonic
self.issue_fd = issue_fd; self.policy_path = policy_path; self.deadline_monotonic = deadline_monotonic
def check(self, plan):
import math, os, stat, subprocess, time
import math, time
validate_plan(plan)
if (type(self.issue_fd) is not int or type(self.deadline_monotonic) not in [int, float]
or not math.isfinite(self.deadline_monotonic) or time.monotonic() >= self.deadline_monotonic):
fail('invalid or expired Issue lease')
t = plan['target']
info = _support.validate_cache_paths(self.policy_path, t['owner'], t['repository'], t['issue'])
t = plan['target']; info = _support.validate_cache_paths(self.policy_path, t['owner'], t['repository'], t['issue'])
lock = Path(info['locks']) / f'issue-{t["issue"]}.lock'
try:
current = os.fstat(self.issue_fd); disk = lock.lstat()
if (not stat.S_ISREG(current.st_mode) or not stat.S_ISREG(disk.st_mode)
or current.st_uid != os.getuid() or stat.S_IMODE(current.st_mode) != 0o600
or current.st_nlink != 1 or (current.st_dev, current.st_ino) != (disk.st_dev, disk.st_ino)):
fail('invalid Issue lock descriptor')
# Linux fdinfo reports locks on this open file description. Do not
# acquire/reacquire here: an opened but unheld FD is not a lease.
records = [line.split() for line in Path(f'/proc/self/fdinfo/{self.issue_fd}').read_text().splitlines()]
device_inode = f'{os.major(current.st_dev):02x}:{os.minor(current.st_dev):02x}:{current.st_ino}'
if not any(len(row) == 9 and row[0] == 'lock:'
and row[2:6] == ['FLOCK', 'ADVISORY', 'WRITE', str(os.getpid())]
and row[6:] == [device_inode, '0', 'EOF'] for row in records):
if not _locks.validate_held_flock(self.issue_fd, lock):
fail('Issue lock is not already held by this process')
except OSError:
except (OSError, _locks.LockValidationError):
raise ValidationError('Issue lock unavailable') from None
wt = Path(info['repository']) / 'worktrees' / f'issue-{t["issue"]}' / 'checkout'
for directory in [wt.parent.parent, wt.parent, wt]:
st = directory.lstat()
if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid() or st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
fail('unsafe existing worktree path')
state_file = wt.parent / 'state.json'; st = state_file.lstat()
if not stat.S_ISREG(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o600:
fail('unsafe worktree state')
state = loads_json(state_file.read_text(encoding='utf-8'))
if (type(state) is not dict or state.get('schema') != 'gitea-agent.worktree/v1' or state.get('owner') != t['owner']
or state.get('repository') != t['repository'] or type(state.get('issue')) is not int
or state['issue'] != t['issue'] or state.get('created_by_role') != 'gitea-impl-agent'
or state.get('mode') != 'detached'):
fail('worktree registration identity mismatch')
st = (wt / '.git').lstat()
if not stat.S_ISREG(st.st_mode) or st.st_uid != os.getuid():
fail('invalid worktree Git pointer')
git_binary = '/usr/bin/git'
binary_stat = Path(git_binary).lstat()
if not stat.S_ISREG(binary_stat.st_mode) or binary_stat.st_uid != 0 or binary_stat.st_mode & 0o022:
fail('untrusted system Git binary')
env = {'PATH': '/usr/bin:/bin', 'GIT_CONFIG_GLOBAL': '/dev/null',
'GIT_CONFIG_NOSYSTEM': '1', 'GIT_OPTIONAL_LOCKS': '0'}
def git(*args):
try:
process = subprocess.run([git_binary, '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null',
'-c', 'safe.directory=' + str(wt), *args],
capture_output=True, text=True, env=env, timeout=15)
except Exception:
raise ValidationError('read-only Git check unavailable') from None
if process.returncode or len(process.stdout) > 1000000:
fail('read-only Git check failed')
return process.stdout
if git('-C', str(wt), 'rev-parse', 'HEAD').strip() != t['head']:
fail('worktree HEAD mismatch')
if git('-C', str(wt), 'status', '--porcelain=v1', '--untracked-files=all').strip():
fail('existing worktree is not clean')
if git('-C', str(wt), 'rev-parse', '--path-format=absolute', '--git-common-dir').strip() != info['repository_git']:
fail('worktree common Git directory mismatch')
records = git('--git-dir=' + info['repository_git'], 'worktree', 'list', '--porcelain', '-z').split('\0\0')
matches = [r.split('\0') for r in records if r.startswith('worktree ' + str(wt) + '\0')]
if len(matches) != 1 or 'detached' not in matches[0] or not any(x == 'locked' or x.startswith('locked ') for x in matches[0]):
fail('worktree detached/locked registration mismatch')
if time.monotonic() >= self.deadline_monotonic:
fail('Issue lease expired during source checks')
_validate_registered_source(plan, self.policy_path, self.deadline_monotonic)
class BrokerIssueLease:
"""Validate an authenticated shared managed-lock broker Issue lease."""
def __init__(self, handle_path, policy_path, deadline_monotonic):
self.handle_path = handle_path; self.policy_path = policy_path; self.deadline_monotonic = deadline_monotonic
def check(self, plan):
import math, time
validate_plan(plan)
if (type(self.deadline_monotonic) not in [int, float] or not math.isfinite(self.deadline_monotonic)
or time.monotonic() >= self.deadline_monotonic):
fail('invalid or expired Issue lease')
t = plan['target']
try:
handle = _broker.read_handle_metadata(self.handle_path)
status = _broker.request_handle(self.handle_path, 'status')
except Exception:
raise ValidationError('broker Issue lease unavailable') from None
if (type(status) is not dict or status.get('status') not in ('held', 'repository-held')
or status.get('owner') != t['owner'] or status.get('repository') != t['repository']
or status.get('issue') != t['issue'] or status.get('role') != 'gitea-review-agent'
or type(status.get('pid')) is not int or status.get('pid') != handle.get('pid')
or type(handle.get('pid')) is not int or status.get('deadline') != handle.get('deadline')
or type(status.get('deadline')) not in [int, float]
or status.get('deadline') > self.deadline_monotonic or time.monotonic() >= status.get('deadline')):
fail('broker Issue lease identity mismatch')
_validate_registered_source(plan, self.policy_path, self.deadline_monotonic)
def main(argv=None):