Track tracefs fd and cwd state
ober
cd9a7862595b4f9db6807fc32ad4ff61251c4281
--- a/docs/limits-followup.md +++ b/docs/limits-followup.md @@ -1,7 +1,7 @@ # Limits Follow-Up: Missing Enforcement and Test Work Reviewed: 2026-05-21 -Status updated: 2026-05-21 (post child-status handshake phase) +Status updated: 2026-05-22 (tracefs fd/cwd state phase) This document is a follow-up to `docs/limits.md` after reviewing the new limits/sandbox/audit module set added around commit `97eea41`. @@ -10,9 +10,8 @@ The short version after the child-status and tracefs-normalization phases: the new modules are wired far enough for callers to choose safe fail-closed behavior based on actual pre-exec child setup status. The focused primitive tests are part of `make test`. Some backend work remains: Linux cgroup -enforcement is not implemented, tracefs still lacks fd/cwd state tracking and -native backends, and deeper backend-specific filesystem and network denial -tests are still needed. +enforcement is not implemented, tracefs still lacks native backends and deeper +backend-specific filesystem and network denial tests are still needed. ## Status After Follow-Up Fixes @@ -22,7 +21,7 @@ tests are still needed. | 2 | Process supervision | DONE — timeout/capture handling, `128 + signal` statuses, output caps, and pre-exec go/no-go handshakes are covered by focused tests | | 3 | Resource limits | SAFE PARTIAL — requested-limit plans and child install reports are per-kind; `require: '(limits)` fails closed before exec; parent-side time/output markers exist; **GAP**: cgroup v2 | | 4 | Executable identity | DONE — path search, realpath/stat identity, comparison helper, and TOCTOU caveat are present | -| 5 | Filesystem tracing | SAFE PARTIAL — `tracefs-capabilities`, fail-closed wrapper, normalized `fs-event` output, and generic read/write/exec suggestions exist; **GAP**: fd/cwd tracking and native backends | +| 5 | Filesystem tracing | SAFE PARTIAL — `tracefs-capabilities`, fail-closed wrapper, normalized `fs-event` output, best-effort fd/cwd tracking, and generic read/write/exec suggestions exist; **GAP**: native backends and deeper Linux traced-run tests | | 6 | Network allowlist | DONE for decision layer — IP literals and localnet ranges are denied before wildcard matching; **GAP**: DNS recheck + child network sandbox/proxy handoff | | 7 | Environment/secrets | DONE — default deny policy, argv validation, env construction, and redaction helpers exist | | 8 | Temp HOME/cache | DONE for helper layer — fake HOME, scratch/cache grants, env overrides, cleanup, and sandbox grant helper exist | @@ -268,6 +267,9 @@ Implemented: - summary by syscall/path. - normalized `fs-event` records with operation classes such as `read`, `write`, `create`, `delete`, `rename`, `metadata`, `readlink`, `symlink`, and `exec`. +- best-effort cwd/fd state reconstruction from complete `strace` lines, + including relative `openat`, fd annotations from `strace -y`, directory fd + mappings, `chdir`/`fchdir`, `close`, `dup*`, and fork/clone inheritance. - generic policy suggestion grouping as `((read . paths) (write . paths) (exec . paths))`, reusable by `jsh` or other callers. @@ -276,22 +278,23 @@ Missing relative to `docs/limits.md`: - On macOS this reports `unavailable`; no fallback tracing is implemented. - No native ptrace/eBPF backend. - No dtrace, EndpointSecurity, ktrace, truss, or Capsicum-aware backend. -- The parser does not maintain enough process state for full path resolution: - cwd per process, fd table, fd inheritance, openat directory fd mappings, and - exec identity changes are still missing. +- The parser reconstructs cwd/fd state best-effort from complete trace lines, + but it is still limited by missing/unfinished `strace` entries and by paths + that never appear in either path arguments or fd annotations. - `tracefs-strace-cmd` silently returns the raw target command when tracing is unavailable. That can look like success unless the caller separately checks `tracefs-mode`. Required next work: -1. Add an explicit `tracefs-capabilities` report. -2. Make unavailable tracing impossible to confuse with successful empty traces. -3. Track cwd and fd state for Linux `strace`. +1. DONE — add an explicit `tracefs-capabilities` report. +2. DONE — make unavailable tracing impossible to confuse with successful empty traces when callers use the fail-closed wrapper. +3. DONE — track cwd and fd state for Linux `strace` on a best-effort basis. 4. DONE — normalize events into the design's `fs-event` format. -5. Add tests for stat, rename, relative paths, openat directory-fd state, and - forked children. Basic open/create/unlink/exec normalization and policy - suggestions are covered by focused primitive tests. +5. Add tests for stat, rename, and forked traced children. Basic + open/create/unlink/exec normalization, relative cwd paths, fd annotations, + openat directory-fd state, and policy suggestions are covered by focused + primitive tests. ## 6. Network Allowlist Proxy --- a/lib/std/os/tracefs.ss +++ b/lib/std/os/tracefs.ss @@ -143,7 +143,7 @@ [installed? '("strace -f imposes ~10x overhead on syscall-heavy workloads" "parser only handles complete lines; partial <unfinished> entries are dropped" - "path resolution does not track per-process cwd or fd table; relative paths render as-written")] + "fd/cwd state is reconstructed best-effort from complete trace lines and strace -y annotations")] [(platform-linux?) '("strace binary not on PATH; install it (e.g. apt install strace, dnf install strace)")] [(platform-macos?) @@ -698,6 +698,157 @@ (string=? sc "creat")) rc))) + (def (tracefs-absolute-path? p) + (and (string? p) + (> (string-length p) 0) + (char=? (string-ref p 0) #\/))) + + (def (tracefs-path-join base rel) + (cond + [(not (string? rel)) rel] + [(tracefs-absolute-path? rel) rel] + [(or (not base) (= (string-length base) 0)) rel] + [(string=? base "/") (string-append "/" rel)] + [(char=? (string-ref base (- (string-length base) 1)) #\/) + (string-append base rel)] + [else (string-append base "/" rel)])) + + (def (tracefs-at-syscall? syscall) + (or (string=? syscall "openat") + (string=? syscall "openat2") + (string=? syscall "fstatat") + (string=? syscall "newfstatat") + (string=? syscall "faccessat") + (string=? syscall "faccessat2") + (string=? syscall "unlinkat") + (string=? syscall "mkdirat") + (string=? syscall "mknodat") + (string=? syscall "readlinkat") + (string=? syscall "fchmodat") + (string=? syscall "fchownat") + (string=? syscall "utimensat") + (string=? syscall "execveat") + (string=? syscall "renameat") + (string=? syscall "renameat2") + (string=? syscall "linkat") + (string=? syscall "symlinkat"))) + + (def (tracefs-fd-key pid fd) + (cons pid fd)) + + (def (tracefs-fd-table-ref fd-table pid fd) + (and (integer? fd) + (hashtable-ref fd-table (tracefs-fd-key pid fd) #f))) + + (def (tracefs-fd-table-set! fd-table pid fd path) + (when (and (integer? fd) path) + (hashtable-set! fd-table (tracefs-fd-key pid fd) path))) + + (def (tracefs-fd-table-delete! fd-table pid fd) + (when (integer? fd) + (hashtable-delete! fd-table (tracefs-fd-key pid fd)))) + + (def (tracefs-copy-process-state! cwd-table fd-table parent child) + (let ([cwd (hashtable-ref cwd-table parent #f)]) + (when cwd (hashtable-set! cwd-table child cwd))) + (for-each + (lambda (key) + (when (and (pair? key) (equal? (car key) parent)) + (hashtable-set! fd-table + (tracefs-fd-key child (cdr key)) + (hashtable-ref fd-table key #f)))) + (vector->list (hashtable-keys fd-table)))) + + (def (tracefs-parse-leading-fd raw) + (and raw + (let ([s raw] [n (string-length raw)]) + (let lp ([i 0]) + (cond + [(= i n) + (and (> i 0) (string->number (substring s 0 i)))] + [(char-numeric? (string-ref s i)) + (lp (+ i 1))] + [(= i 0) #f] + [else (string->number (substring s 0 i))]))))) + + (def (tracefs-fd-annotation-path raw) + (and raw + (let ([start (find-char raw #\<)]) + (and start + (let ([rest (substring raw (+ start 1) (string-length raw))]) + (let ([end (find-char rest #\>)]) + (and end + (let ([p (substring rest 0 end)]) + (and (tracefs-absolute-path? p) p))))))))) + + (def (tracefs-dirfd-base pid syscall args cwd-table fd-table) + (cond + [(not (tracefs-at-syscall? syscall)) + (hashtable-ref cwd-table pid #f)] + [else + (let ([raw (list-ref/default args 0 #f)]) + (cond + [(or (not raw) (string=? raw "AT_FDCWD")) + (hashtable-ref cwd-table pid #f)] + [(tracefs-fd-annotation-path raw) => + (lambda (p) p)] + [else + (let ([fd (tracefs-parse-leading-fd raw)]) + (or (tracefs-fd-table-ref fd-table pid fd) + (hashtable-ref cwd-table pid #f)))]))])) + + (def (tracefs-resolve-event-path pid syscall args path cwd-table fd-table) + (cond + [(not path) #f] + [(tracefs-absolute-path? path) path] + [else + (tracefs-path-join + (tracefs-dirfd-base pid syscall args cwd-table fd-table) + path)])) + + (def (tracefs-fd-only-ops syscall args) + (cond + [(or (string=? syscall "read") + (string=? syscall "readv") + (string=? syscall "pread64")) + '(read)] + [(or (string=? syscall "write") + (string=? syscall "writev") + (string=? syscall "pwrite64")) + '(write)] + [(string=? syscall "fstat") + '(metadata)] + [(or (string=? syscall "ftruncate") + (string=? syscall "fchmod") + (string=? syscall "fchown") + (string=? syscall "fsync") + (string=? syscall "fdatasync")) + '(write)] + [(or (string=? syscall "getdents") + (string=? syscall "getdents64")) + '(readdir)] + [else '()])) + + (def (tracefs-fd-event-path pid args fd-table) + (let* ([raw (list-ref/default args 0 #f)] + [annotated (tracefs-fd-annotation-path raw)] + [fd (tracefs-parse-leading-fd raw)]) + (or annotated + (tracefs-fd-table-ref fd-table pid fd)))) + + (def (make-fs-event-from-trace/fd e op path fd) + (make-fs-event-rec + (trace-event-pid e) + #f + #f + op + path + #f + fd + (tracefs-result-symbol e) + (trace-event-errno e) + (trace-event-ts e))) + (def (make-fs-event-from-trace e op path) (make-fs-event-rec (trace-event-pid e) @@ -711,6 +862,89 @@ (trace-event-errno e) (trace-event-ts e))) + (def (trace-event->fs-events/state e cwd-table fd-table) + (let* ([pid (trace-event-pid e)] + [syscall (trace-event-syscall e)] + [args (trace-event-args e)] + [fd-ops (tracefs-fd-only-ops syscall args)]) + (cond + [(pair? fd-ops) + (let* ([path (tracefs-fd-event-path pid args fd-table)] + [fd (tracefs-parse-leading-fd (list-ref/default args 0 #f))]) + (if path + (map (lambda (op) + (make-fs-event-from-trace/fd e op path fd)) + fd-ops) + '()))] + [else + (map (lambda (ev) + (make-fs-event-rec + (fs-event-pid ev) + (fs-event-ppid ev) + (fs-event-exe ev) + (fs-event-op ev) + (tracefs-resolve-event-path pid syscall args + (fs-event-path ev) + cwd-table fd-table) + (hashtable-ref cwd-table pid #f) + (fs-event-fd ev) + (fs-event-result ev) + (fs-event-errno ev) + (fs-event-timestamp-ms ev))) + (trace-event->fs-events e))]))) + + (def (tracefs-update-state! e cwd-table fd-table) + (let* ([pid (trace-event-pid e)] + [syscall (trace-event-syscall e)] + [args (trace-event-args e)] + [rc (trace-event-result e)]) + (when (tracefs-success? e) + (cond + [(or (string=? syscall "fork") + (string=? syscall "vfork") + (string=? syscall "clone")) + (when (and (integer? rc) (> rc 0)) + (tracefs-copy-process-state! cwd-table fd-table pid rc))] + [(string=? syscall "chdir") + (let ([p (tracefs-resolve-event-path + pid syscall args + (tracefs-arg->path (list-ref/default args 0 #f)) + cwd-table fd-table)]) + (when p (hashtable-set! cwd-table pid p)))] + [(string=? syscall "fchdir") + (let* ([fd (tracefs-parse-leading-fd (list-ref/default args 0 #f))] + [p (tracefs-fd-table-ref fd-table pid fd)]) + (when p (hashtable-set! cwd-table pid p)))] + [(or (string=? syscall "open") + (string=? syscall "openat") + (string=? syscall "openat2") + (string=? syscall "creat")) + (let ([fd (tracefs-fd-result e)]) + (when fd + (let* ([paths (tracefs-event-paths syscall args + (trace-event-path e))] + [p (and (pair? paths) + (tracefs-resolve-event-path + pid syscall args (car paths) + cwd-table fd-table))]) + (tracefs-fd-table-set! fd-table pid fd p))))] + [(string=? syscall "close") + (tracefs-fd-table-delete! + fd-table pid + (tracefs-parse-leading-fd (list-ref/default args 0 #f)))] + [(or (string=? syscall "dup") + (string=? syscall "dup2") + (string=? syscall "dup3")) + (let* ([old-fd (tracefs-parse-leading-fd + (list-ref/default args 0 #f))] + [new-fd (if (string=? syscall "dup") + rc + (tracefs-parse-leading-fd + (list-ref/default args 1 #f)))] + [p (tracefs-fd-table-ref fd-table pid old-fd)]) + (tracefs-fd-table-set! fd-table pid new-fd p))] + [else (void)])))) + (def (trace-event->fs-events e) (let* ([syscall (trace-event-syscall e)] [args (trace-event-args e)] @@ -732,17 +966,23 @@ out2))]))]))]))) (def (tracefs-normalize-events events) - (let lp ([xs events] [out '()]) - (cond - [(null? xs) (reverse out)] - [(fs-event? (car xs)) - (lp (cdr xs) (cons (car xs) out))] - [(trace-event? (car xs)) - (let add ([ys (trace-event->fs-events (car xs))] [acc out]) - (cond - [(null? ys) (lp (cdr xs) acc)] - [else (add (cdr ys) (cons (car ys) acc))]))] - [else (lp (cdr xs) out)]))) + (let ([cwd-table (make-eqv-hashtable)] + [fd-table (make-hashtable equal-hash equal?)]) + (let lp ([xs events] [out '()]) + (cond + [(null? xs) (reverse out)] + [(fs-event? (car xs)) + (lp (cdr xs) (cons (car xs) out))] + [(trace-event? (car xs)) + (let ([e (car xs)]) + (let add ([ys (trace-event->fs-events/state e cwd-table fd-table)] + [acc out]) + (cond + [(null? ys) + (tracefs-update-state! e cwd-table fd-table) + (lp (cdr xs) acc)] + [else (add (cdr ys) (cons (car ys) acc))])))] + [else (lp (cdr xs) out)])))) (def (unique-sorted-strings xs) (let lp ([rest xs] [seen '()] [out '()]) --- a/tests/test-limits-primitives.ss +++ b/tests/test-limits-primitives.ss @@ -435,6 +435,17 @@ #t] [else (lp (cdr xs))]))) +(define (has-fs-event-with-fd? events op path fd) + (let lp ([xs events]) + (cond + [(null? xs) #f] + [(and (fs-event? (car xs)) + (eq? (fs-event-op (car xs)) op) + (equal? (fs-event-path (car xs)) path) + (equal? (fs-event-fd (car xs)) fd)) + #t] + [else (lp (cdr xs))]))) + (define (has-path? xs path) (let lp ([rest xs]) (cond @@ -489,6 +500,30 @@ (cdr (assq 'read suggest)) (lambda (xs) (not (has-path? xs "/repo/missing"))))) +(let* ([raw (tracefs-parse-strace + (open-input-string + (string-append + "100 chdir(\"/repo\") = 0\n" + "100 openat(AT_FDCWD, \"package.json\", O_RDONLY|O_CLOEXEC) = 3\n" + "100 read(3</repo/package.json>, \"{}\", 2) = 2\n" + "100 openat(AT_FDCWD, \"out.txt\", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4\n" + "100 write(4</repo/out.txt>, \"x\", 1) = 1\n" + "100 openat(AT_FDCWD, \"/repo/subdir\", O_RDONLY|O_DIRECTORY) = 5\n" + "100 openat(5</repo/subdir>, \"child.txt\", O_RDONLY) = 6\n")))] + [events (tracefs-normalize-events raw)]) + (test-pred "tracefs cwd tracking resolves relative openat" + events + (lambda (xs) (has-fs-event? xs 'read "/repo/package.json"))) + (test-pred "tracefs fd annotation emits fd read event" + events + (lambda (xs) (has-fs-event-with-fd? xs 'read "/repo/package.json" 3))) + (test-pred "tracefs fd annotation emits fd write event" + events + (lambda (xs) (has-fs-event-with-fd? xs 'write "/repo/out.txt" 4))) + (test-pred "tracefs dirfd tracking resolves relative openat" + events + (lambda (xs) (has-fs-event? xs 'read "/repo/subdir/child.txt")))) + ;; ===== temp-home: sandbox integration ===== (printf "[temp-home]~%")