#!/usr/bin/env python3 """Read or write the ownership sweep sentinel without following symlinks. The sentinel lives in /config, which the unprivileged runtime user owns, so it can be swapped for a symlink. read trusts only a root-owned regular file; write never follows a symlink or fifo onto another file. Usage: safe-sentinel read PATH print content, exit 0 only if root-owned regular file safe-sentinel write PATH CONTENT write CONTENT to a regular file at PATH """ import errno import os import stat import sys MODE = 0o644 def do_read(path: str) -> int: try: fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) except OSError: return 1 try: st = os.fstat(fd) if not stat.S_ISREG(st.st_mode) or st.st_uid != 0: return 1 sys.stdout.buffer.write(os.read(fd, 4096)) finally: os.close(fd) return 0 def do_write(path: str, content: str) -> int: # O_NONBLOCK so a fifo fails fast (ENXIO) instead of blocking the open. flags = os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK replace = (errno.ELOOP, errno.ENXIO) try: fd = os.open(path, flags, MODE) if not stat.S_ISREG(os.fstat(fd).st_mode): os.close(fd) raise OSError(errno.ELOOP, "not a regular file") except OSError as err: if err.errno not in replace: raise os.unlink(path) fd = os.open(path, flags | os.O_EXCL, MODE) try: os.ftruncate(fd, 0) os.write(fd, content.encode()) # keep it root-owned so a later sweep that chowned the old sentinel to # the runtime user can't make the next read reject and re-sweep os.fchown(fd, 0, 0) finally: os.close(fd) return 0 def main(argv: list[str]) -> int: if len(argv) == 3 and argv[1] == "read": return do_read(argv[2]) if len(argv) == 4 and argv[1] == "write": try: return do_write(argv[2], argv[3]) except OSError: return 1 print("usage: safe-sentinel read PATH | write PATH CONTENT", file=sys.stderr) return 2 if __name__ == "__main__": sys.exit(main(sys.argv))