.
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 4 Nov 2026 05:59:36 +0000 (05:59 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 4 Nov 2026 05:59:36 +0000 (05:59 +0000)
developer/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c [deleted file]
developer/authored/Man_In_Grey.copy/FileLanguage.py
developer/authored/Man_In_Grey.copy/journal_interpreter_file.py
developer/authored/Man_In_Grey_file_gate.make/Man_In_Grey_file_gate.CLI.c [new file with mode: 0644]

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 (file)
index eeb0114..0000000
+++ /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 <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));
-}
index e937760..0a7bab1 100644 (file)
@@ -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)
index 30f900b..56109d4 100755 (executable)
@@ -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 <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
 """
@@ -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 (file)
index 0000000..7dd35c2
--- /dev/null
@@ -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 <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));
+}