From 62b1bc9672f72a5d6172487c538cf6fe629b267a Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Wed, 4 Nov 2026 05:59:36 +0000 Subject: [PATCH] . --- .../authored/Man_In_Grey.copy/FileLanguage.py | 13 ++++-- .../journal_interpreter_file.py | 45 ++++++++++--------- .../Man_In_Grey_file_gate.CLI.c | 25 +++++++---- 3 files changed, 51 insertions(+), 32 deletions(-) rename developer/{ => authored}/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c (82%) diff --git a/developer/authored/Man_In_Grey.copy/FileLanguage.py b/developer/authored/Man_In_Grey.copy/FileLanguage.py index e937760..0a7bab1 100644 --- a/developer/authored/Man_In_Grey.copy/FileLanguage.py +++ b/developer/authored/Man_In_Grey.copy/FileLanguage.py @@ -288,14 +288,21 @@ def execute(data ,log): for index in sorted(opened): record = opened[index] argument ,dirfd ,fd ,tmp = record["argument"] ,record["dirfd"] ,record["fd"] ,record["tmp"] + target = argument["name"] + where = f"{argument['directory']}/{target}" account = pwd.getpwnam(argument["owner"]) os.fsync(fd) - os.fchown(fd ,account.pw_uid ,account.pw_gid) + # Only root can give a file away. Any other interpreter account makes + # files it already owns, so the owner is changed only when it differs. + st = os.fstat(fd) + if (st.st_uid ,st.st_gid) != (account.pw_uid ,account.pw_gid): + try: + os.fchown(fd ,account.pw_uid ,account.pw_gid) + except OSError as error: + raise OSError(error.errno ,f"{where}: cannot give it the owner {argument['owner']} and that account's group: {error.strerror}") os.fchmod(fd ,argument["permission"]) os.fsync(fd) os.close(fd); record["fd"] = None - target = argument["name"] - where = f"{argument['directory']}/{target}" if argument["mode"] == "create": os.link(tmp ,target ,src_dir_fd=dirfd ,dst_dir_fd=dirfd ,follow_symlinks=False) os.unlink(tmp ,dir_fd=dirfd) diff --git a/developer/authored/Man_In_Grey.copy/journal_interpreter_file.py b/developer/authored/Man_In_Grey.copy/journal_interpreter_file.py index 30f900b..56109d4 100755 --- a/developer/authored/Man_In_Grey.copy/journal_interpreter_file.py +++ b/developer/authored/Man_In_Grey.copy/journal_interpreter_file.py @@ -6,15 +6,17 @@ journal_interpreter_file The bespoke journal interpreter for the file command language, started by its gate, Man_In_Grey_file_gate, with the journal on standard input. -Its constraints are the file constraints_file.json beside it. When it runs -privileged, that file, this program, and the modules it loads are to be -owned by root and writable by no one else, or it refuses to run. When it runs -unprivileged, through a gate that was not blessed, the caller can give other +Its constraints are the file constraints_file.json beside it. On every run, +that file, this program, the modules it loads, and the directory holding +them are to be owned by root or by the interpreter account, the account +this program runs as, and writable by no other account, or it refuses to +run. Through a gate that was not blessed, the caller can give other constraints with --constraints, since nothing then runs with more than the -caller's own rights. +caller's own rights; that file is checked the same way. Usage: journal_interpreter_file [--constraints ] [--print] [--stop-after ] - --constraints constraints to use in place of constraints_file.json; unprivileged only + --constraints constraints to use in place of constraints_file.json; refused when the + gate changed the account or the interpreter account is root --print print the journal as decoded --stop-after stop after decode, wellformed, constraint, validity or filesystem """ @@ -33,25 +35,25 @@ MODULE = ("Journal.py" ,"FileLanguage.py" ,"journal_interpreter.py" ,"journal_in def privileged(): return os.geteuid() != os.getuid() or os.getegid() != os.getgid() -def owned_by_root_problem(path): +def trusted_owner_problem(path): + "None when path is owned by root or the interpreter account and writable by no other account" try: st = os.lstat(path) except OSError as error: return f"{path}: {error.strerror}" - if st.st_uid != 0: - return f"{path} is not owned by root" + if st.st_uid not in (0 ,os.geteuid()): + return f"{path} is owned by uid {st.st_uid}, neither root nor the interpreter account" if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH): - return f"{path} is writable by an account other than root" + return f"{path} is writable by group or other accounts" return None -# Running privileged, every file this program loads is checked before any of -# it is imported: a module runs code when it is imported. -if privileged(): - for each in [HERE_DPATH ,CONSTRAINT_FILE] + [HERE_DPATH / m for m in MODULE]: - problem = owned_by_root_problem(each) - if problem: - print(f"error: refusing to run privileged: {problem}" ,file=sys.stderr) - sys.exit(2) +# Every file this program loads is checked before any of it is imported, on +# every run: a module runs code when it is imported. +for each in [HERE_DPATH ,CONSTRAINT_FILE] + [HERE_DPATH / m for m in MODULE]: + problem = trusted_owner_problem(each) + if problem: + print(f"error: refusing to run: {problem}" ,file=sys.stderr) + sys.exit(2) import journal_interpreter import FileLanguage @@ -59,9 +61,12 @@ import FileLanguage def load_constraint(given): if given is not None: - if privileged(): - raise RuntimeError("--constraints is refused when running privileged") + if privileged() or os.geteuid() == 0: + raise RuntimeError("--constraints is refused when the gate changed the account or the interpreter account is root") path = Path(given) + problem = trusted_owner_problem(path) + if problem: + raise RuntimeError(f"refusing the constraints: {problem}") else: path = CONSTRAINT_FILE with open(path ,encoding="utf-8") as f: diff --git a/developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c b/developer/authored/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c similarity index 82% rename from developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c rename to developer/authored/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c index eeb0114..7dd35c2 100644 --- a/developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c +++ b/developer/authored/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c @@ -8,9 +8,10 @@ The journal interpreter is at a path fixed when the gate is compiled, relative to the gate: ../Man_In_Grey/journal_interpreter_file.py. The caller - cannot name another. When running privileged, the gate refuses to start it - unless the interpreter, and every directory above it, is owned by root and - writable by no other account. + cannot name another. The gate refuses to start it unless the interpreter, + and every directory above it, is owned by root or by the interpreter + account, and writable by no other account. The check is made on every run, + blessed or not. The journal arrives on standard input and is passed on unread. Every argument is passed on to the journal interpreter, except --print-flags, @@ -50,8 +51,12 @@ Local bool privileged(void){ return geteuid() != getuid() || getegid() != getgid(); } -/* path and every directory above it: owned by root, writable by no other account */ -Local void check_owned_by_root(const char *path){ +/* + path and every directory above it: owned by root or by the interpreter + account, the effective uid, and writable by no other account +*/ +Local void check_trusted_owner(const char *path){ + uid_t interpreter_uid = geteuid(); char prefix[PATH_MAX]; size_t length = strlen(path); if(length >= sizeof prefix) die("path too long: %s" ,path); @@ -60,11 +65,13 @@ Local void check_owned_by_root(const char *path){ memcpy(prefix ,path ,i); prefix[i] = '\0'; struct stat st; if( lstat(prefix ,&st) != 0 ) die("cannot examine %s: %s" ,prefix ,strerror(errno)); - if( st.st_uid != 0 ) die("refusing to run privileged: %s is not owned by root" ,prefix); - if( st.st_mode & (S_IWGRP | S_IWOTH) ) die("refusing to run privileged: %s is writable by an account other than root" ,prefix); + if( st.st_uid != 0 && st.st_uid != interpreter_uid ) + die("refusing to run: %s is owned by uid %ld, neither root nor the interpreter account" ,prefix ,(long)st.st_uid); + if( st.st_mode & (S_IWGRP | S_IWOTH) ) + die("refusing to run: %s is writable by group or other accounts" ,prefix); } struct stat st; - if( lstat("/" ,&st) != 0 || st.st_uid != 0 ) die("refusing to run privileged: / is not owned by root"); + if( lstat("/" ,&st) != 0 || st.st_uid != 0 ) die("refusing to run: / is not owned by root"); } Local void print_flags(void){ @@ -90,7 +97,7 @@ int main(int argc ,char **argv){ char interpreter[PATH_MAX]; if( !realpath(candidate ,interpreter) ) die("journal interpreter not found: %s" ,candidate); - if( privileged() ) check_owned_by_root(interpreter); + check_trusted_owner(interpreter); /* harden the process before starting the interpreter */ uid_t caller_uid = getuid(); -- 2.20.1