93 lines
2.9 KiB
Python
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
|