+++ /dev/null
-/*
- 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 <errno.h>
-#include <libgen.h>
-#include <limits.h>
-#include <pwd.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/prctl.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#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 <interpreter> <arguments> */
- 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));
-}
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)
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 <file>] [--print] [--stop-after <check>]
- --constraints <file> constraints to use in place of constraints_file.json; unprivileged only
+ --constraints <file> 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 <check> stop after decode, wellformed, constraint, validity or filesystem
"""
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
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:
--- /dev/null
+/*
+ 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 <errno.h>
+#include <libgen.h>
+#include <limits.h>
+#include <pwd.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#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 <interpreter> <arguments> */
+ 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));
+}