From: Thomas Walker Lynch Date: Wed, 4 Nov 2026 05:59:36 +0000 (+0000) Subject: . X-Git-Url: https://git.reasoningtechnology.com/%27%20%20%20window.RT.dirpr_library%20%20%20%27/USER_NAME.png?a=commitdiff_plain;h=62b1bc9672f72a5d6172487c538cf6fe629b267a;p=Man_In_Grey . --- diff --git a/developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c b/developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c deleted file mode 100644 index eeb0114..0000000 --- a/developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c +++ /dev/null @@ -1,127 +0,0 @@ -/* - Man_In_Grey_file_gate - - The gate of the bespoke journal interpreter for the file command language. - Installed setuid root, it starts the journal interpreter with root's rights - while the real uid still names the caller. Unblessed, it starts the journal - interpreter with the caller's rights. - - 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. - - The journal arrives on standard input and is passed on unread. Every - argument is passed on to the journal interpreter, except --print-flags, - which reports the caller and whether the gate is running privileged. - - Who is allowed to hand a journal to the interpreter is decided by the - interpreter's constraints, not here. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define PYTHON "/usr/bin/python3" -#define INTERPRETER_RELATIVE "../Man_In_Grey/journal_interpreter_file.py" -#define ARGUMENT_MAX 64 - -Local void die(const char *format ,...){ - va_list ap; va_start(ap ,format); - fputs("Man_In_Grey_file_gate: " ,stderr); - vfprintf(stderr ,format ,ap); va_end(ap); - fputc('\n' ,stderr); - _exit(2); -} - -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){ - char prefix[PATH_MAX]; - size_t length = strlen(path); - if(length >= sizeof prefix) die("path too long: %s" ,path); - for(size_t i = 1; i <= length; ++i){ - if( i < length && path[i] != '/' ) continue; - 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); - } - struct stat st; - if( lstat("/" ,&st) != 0 || st.st_uid != 0 ) die("refusing to run privileged: / is not owned by root"); -} - -Local void print_flags(void){ - struct passwd *pw = getpwuid(getuid()); - printf("caller.uid=%ld\n" ,(long)getuid()); - printf("caller.name=%s\n" ,pw ? pw->pw_name : "unknown"); - printf("effective.uid=%ld\n" ,(long)geteuid()); - printf("privileged=%d\n" ,privileged() ? 1 : 0); -} - -int main(int argc ,char **argv){ - if( argc > 1 && strcmp(argv[1] ,"--print-flags") == 0 ){ print_flags(); return 0; } - if( argc > ARGUMENT_MAX ) die("too many arguments"); - - char exe[PATH_MAX]; - ssize_t n = readlink("/proc/self/exe" ,exe ,sizeof exe - 1); - if( n < 0 ) die("cannot resolve /proc/self/exe: %s" ,strerror(errno)); - exe[n] = '\0'; - - char candidate[PATH_MAX]; - if( snprintf(candidate ,sizeof candidate ,"%s/%s" ,dirname(exe) ,INTERPRETER_RELATIVE) >= (int)sizeof candidate ) - die("path too long"); - char interpreter[PATH_MAX]; - if( !realpath(candidate ,interpreter) ) die("journal interpreter not found: %s" ,candidate); - - if( privileged() ) check_owned_by_root(interpreter); - - /* harden the process before starting the interpreter */ - uid_t caller_uid = getuid(); - gid_t caller_gid = getgid(); - struct passwd *pw = getpwuid(caller_uid); - char caller_name[256]; - snprintf(caller_name ,sizeof caller_name ,"%s" ,pw ? pw->pw_name : "unknown"); - - clearenv(); - setenv("PATH" ,"/usr/sbin:/usr/bin:/sbin:/bin" ,1); - setenv("LANG" ,"C.UTF-8" ,1); - umask(077); - prctl(PR_SET_DUMPABLE ,0 ,0 ,0 ,0); - if( chdir("/") != 0 ) die("cannot change directory to /"); - - char number[32]; - snprintf(number ,sizeof number ,"%ld" ,(long)caller_uid); setenv("MIG_CALLER_UID" ,number ,1); - snprintf(number ,sizeof number ,"%ld" ,(long)caller_gid); setenv("MIG_CALLER_GID" ,number ,1); - setenv("MIG_CALLER_NAME" ,caller_name ,1); - - /* python3 -E -s -B */ - char *argument[ARGUMENT_MAX + 6]; - int k = 0; - argument[k++] = (char *)PYTHON; - argument[k++] = (char *)"-E"; - argument[k++] = (char *)"-s"; - argument[k++] = (char *)"-B"; - argument[k++] = interpreter; - for(int i = 1; i < argc; ++i) argument[k++] = argv[i]; - argument[k] = NULL; - - execv(PYTHON ,argument); - die("cannot start %s: %s" ,PYTHON ,strerror(errno)); -} 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/authored/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 new file mode 100644 index 0000000..7dd35c2 --- /dev/null +++ b/developer/authored/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c @@ -0,0 +1,134 @@ +/* + Man_In_Grey_file_gate + + The gate of the bespoke journal interpreter for the file command language. + Installed setuid root, it starts the journal interpreter with root's rights + while the real uid still names the caller. Unblessed, it starts the journal + interpreter with the caller's rights. + + 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. 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, + which reports the caller and whether the gate is running privileged. + + Who is allowed to hand a journal to the interpreter is decided by the + interpreter's constraints, not here. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PYTHON "/usr/bin/python3" +#define INTERPRETER_RELATIVE "../Man_In_Grey/journal_interpreter_file.py" +#define ARGUMENT_MAX 64 + +Local void die(const char *format ,...){ + va_list ap; va_start(ap ,format); + fputs("Man_In_Grey_file_gate: " ,stderr); + vfprintf(stderr ,format ,ap); va_end(ap); + fputc('\n' ,stderr); + _exit(2); +} + +Local bool privileged(void){ + return geteuid() != getuid() || getegid() != getgid(); +} + +/* + 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); + for(size_t i = 1; i <= length; ++i){ + if( i < length && path[i] != '/' ) continue; + 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 && 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: / is not owned by root"); +} + +Local void print_flags(void){ + struct passwd *pw = getpwuid(getuid()); + printf("caller.uid=%ld\n" ,(long)getuid()); + printf("caller.name=%s\n" ,pw ? pw->pw_name : "unknown"); + printf("effective.uid=%ld\n" ,(long)geteuid()); + printf("privileged=%d\n" ,privileged() ? 1 : 0); +} + +int main(int argc ,char **argv){ + if( argc > 1 && strcmp(argv[1] ,"--print-flags") == 0 ){ print_flags(); return 0; } + if( argc > ARGUMENT_MAX ) die("too many arguments"); + + char exe[PATH_MAX]; + ssize_t n = readlink("/proc/self/exe" ,exe ,sizeof exe - 1); + if( n < 0 ) die("cannot resolve /proc/self/exe: %s" ,strerror(errno)); + exe[n] = '\0'; + + char candidate[PATH_MAX]; + if( snprintf(candidate ,sizeof candidate ,"%s/%s" ,dirname(exe) ,INTERPRETER_RELATIVE) >= (int)sizeof candidate ) + die("path too long"); + char interpreter[PATH_MAX]; + if( !realpath(candidate ,interpreter) ) die("journal interpreter not found: %s" ,candidate); + + check_trusted_owner(interpreter); + + /* harden the process before starting the interpreter */ + uid_t caller_uid = getuid(); + gid_t caller_gid = getgid(); + struct passwd *pw = getpwuid(caller_uid); + char caller_name[256]; + snprintf(caller_name ,sizeof caller_name ,"%s" ,pw ? pw->pw_name : "unknown"); + + clearenv(); + setenv("PATH" ,"/usr/sbin:/usr/bin:/sbin:/bin" ,1); + setenv("LANG" ,"C.UTF-8" ,1); + umask(077); + prctl(PR_SET_DUMPABLE ,0 ,0 ,0 ,0); + if( chdir("/") != 0 ) die("cannot change directory to /"); + + char number[32]; + snprintf(number ,sizeof number ,"%ld" ,(long)caller_uid); setenv("MIG_CALLER_UID" ,number ,1); + snprintf(number ,sizeof number ,"%ld" ,(long)caller_gid); setenv("MIG_CALLER_GID" ,number ,1); + setenv("MIG_CALLER_NAME" ,caller_name ,1); + + /* python3 -E -s -B */ + char *argument[ARGUMENT_MAX + 6]; + int k = 0; + argument[k++] = (char *)PYTHON; + argument[k++] = (char *)"-E"; + argument[k++] = (char *)"-s"; + argument[k++] = (char *)"-B"; + argument[k++] = interpreter; + for(int i = 1; i < argc; ++i) argument[k++] = argv[i]; + argument[k] = NULL; + + execv(PYTHON ,argument); + die("cannot start %s: %s" ,PYTHON ,strerror(errno)); +}