Files
archy/scripts/os_update_custody.py

93 lines
2.9 KiB
Python

"""Independent OS-state custody; never repair or take ownership of app data."""
import os
import pathlib
import weakref
import json
from d09_store import atomic, identity, read, secure
from os_update_plan import canonical
LEGACY_ROOT = pathlib.Path('/var/lib/archipelago/os-update')
def refuse_legacy():
"""Inspect only the old entry, never follow it or read/trust its contents."""
try:
parent = os.open(LEGACY_ROOT.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
except FileNotFoundError:
return
except OSError as exc:
raise PermissionError('legacy OS ancestry unresolved; explicit root reconciliation required') from exc
try:
try:
os.stat(LEGACY_ROOT.name, dir_fd=parent, follow_symlinks=False)
except FileNotFoundError:
return
raise PermissionError('legacy OS state exists; explicit root migration/reconciliation required')
finally:
os.close(parent)
def directory_identity(fd):
value = identity(fd)
# Creating a child directory changes nlink, not custody of this directory.
return value[:5] + value[6:]
class Pin:
def __init__(self, path):
self.path = pathlib.Path(path)
self.fd = secure(self.path, True)
self.close = weakref.finalize(self, os.close, self.fd)
self.identity = directory_identity(self.fd)
def check(self):
fd = secure(self.path, True)
try:
if directory_identity(fd) != self.identity or directory_identity(self.fd) != self.identity:
raise PermissionError('OS state directory replaced; reconciliation required')
finally:
os.close(fd)
def mkdir(path):
"""Validate/open protected parent BEFORE creating any child; no parents=True."""
path = pathlib.Path(path)
parent = Pin(path.parent)
try:
try:
os.mkdir(path.name, mode=0o700, dir_fd=parent.fd)
created = True
os.fsync(parent.fd)
except FileExistsError:
created = False
parent.check()
pin = Pin(path)
return pin, created
finally:
parent.close()
def transaction(path, initial, create):
"""Storage only: caller authenticates admission first. Absence is not reset."""
path = pathlib.Path(path)
created = False
pin = None
if create:
pin, created = mkdir(path)
elif path.exists() or path.is_symlink():
pin = Pin(path)
if pin is None:
return None, initial
try:
status = path / 'status.json'
if created:
atomic(status, canonical(initial))
elif not (status.exists() or status.is_symlink()):
raise PermissionError('existing OS transaction has no journal; explicit reconciliation required')
pin.check()
return pin, json.loads(read(status))
except BaseException:
pin.close()
raise