fuse: route STATFS through the access gate (P1 #33c)
ober
0b341b7fbe230f9c3e55260b393ece5ca78018d2
--- a/lib/jerboa-fuse.sls +++ b/lib/jerboa-fuse.sls @@ -36,7 +36,7 @@ FATTR-MTIME-NOW FATTR-CTIME FOPEN-DIRECT-IO FOPEN-KEEP-CACHE FOPEN-NONSEEKABLE make-access-controller access-check access-controller-lock! access-controller-unlock! - access-controller-locked?) + access-controller-locked? always-allowed-opcode?) (import (except (chezscheme) make-hash-table hash-table? sort sort! printf fprintf format path-extension path-absolute? @@ -154,8 +154,7 @@ (def (always-allowed-opcode? op) (or (= op FUSE-INIT) (= op FUSE-DESTROY) - (= op FUSE-INTERRUPT) - (= op FUSE-STATFS))) + (= op FUSE-INTERRUPT))) (def (stealth-deny-response opcode unique nodeid) (cond [(and (= opcode FUSE-GETATTR) (= nodeid FUSE-ROOT-ID)) @@ -169,6 +168,8 @@ (encode-out-header unique 0 0)] [(and (= opcode FUSE-OPENDIR) (= nodeid FUSE-ROOT-ID)) (encode-open-out unique 0 0)] + [(= opcode FUSE-STATFS) + (encode-statfs-out unique (stealth-deny-statfs))] [(= opcode FUSE-RELEASEDIR) (encode-out-header unique 0 0)] [(or (= opcode FUSE-FORGET) (= opcode FUSE-BATCH-FORGET)) #f] --- a/lib/jerboa-fuse/access.sls +++ b/lib/jerboa-fuse/access.sls @@ -5,8 +5,8 @@ (library (jerboa-fuse access) (export make-access-controller access-check access-controller-lock! access-controller-unlock! access-controller-locked? - stealth-deny-attr stealth-deny-readdir access-current-pid - pid-is-descendant?) + stealth-deny-attr stealth-deny-readdir stealth-deny-statfs + access-current-pid pid-is-descendant?) (import (except (chezscheme) make-hash-table hash-table? sort sort! printf fprintf format path-extension path-absolute? @@ -101,4 +101,6 @@ (def (stealth-deny-readdir ino) (list (make-fuse-dirent ino 1 DT-DIR ".") - (make-fuse-dirent ino 2 DT-DIR "..")))) + (make-fuse-dirent ino 2 DT-DIR ".."))) + (def (stealth-deny-statfs) + (make-fuse-statfs 0 0 0 0 0 4096 255 4096))) --- a/src/jerboa-fuse.ss +++ b/src/jerboa-fuse.ss @@ -48,7 +48,10 @@ ;; Re-exports: Access control make-access-controller access-check - access-controller-lock! access-controller-unlock! access-controller-locked?) + access-controller-lock! access-controller-unlock! access-controller-locked? + + ;; Access gate predicate (which opcodes bypass the access controller) + always-allowed-opcode?) (import (jerboa prelude) @@ -225,10 +228,13 @@ ;; Request handler — dispatches a single request ;; ====================================================================== - ;; Opcodes that are always allowed (protocol handshake, no data exposure) + ;; Opcodes that are always allowed (protocol handshake, no data exposure). + ;; FUSE-STATFS is intentionally NOT here: a denied caller would otherwise + ;; learn vault block counts, so it is routed through the access gate and + ;; answered with a generic empty statfs (see stealth-deny-response). (def (always-allowed-opcode? op) (or (= op FUSE-INIT) (= op FUSE-DESTROY) - (= op FUSE-INTERRUPT) (= op FUSE-STATFS))) + (= op FUSE-INTERRUPT))) ;; Stealth deny: denied processes see an empty directory, not errors. ;; GETATTR on root → valid empty dir attr. Everything else → ENOENT. @@ -245,7 +251,10 @@ (encode-out-header unique 0 0)] ;; OPENDIR on root → allow (so readdir works) [(and (= opcode FUSE-OPENDIR) (= nodeid FUSE-ROOT-ID)) - (encode-open-out unique 0 0)] + (encode-open-out unique 0 0)] + ;; STATFS → generic empty stats (deny block-count leak) + [(= opcode FUSE-STATFS) + (encode-statfs-out unique (stealth-deny-statfs))] ;; RELEASEDIR → always OK [(= opcode FUSE-RELEASEDIR) (encode-out-header unique 0 0)] --- a/src/jerboa-fuse/access.ss +++ b/src/jerboa-fuse/access.ss @@ -9,6 +9,7 @@ ;; Stealth deny helpers (for FUSE dispatch) stealth-deny-attr ;; → fuse-attr (empty root-like) stealth-deny-readdir ;; → list of dirents (just . and ..) + stealth-deny-statfs ;; → fuse-statfs (generic/empty, no block counts) ;; Process tree inspection access-current-pid @@ -150,3 +151,9 @@ (make-fuse-dirent ino 1 DT-DIR ".") (make-fuse-dirent ino 2 DT-DIR ".."))) + (def (stealth-deny-statfs) + ;; Generic empty filesystem stats: no block counts are leaked to a + ;; caller denied by the access gate. bsize/namelen/frsize stay sane so + ;; the mountpoint still looks like a filesystem to ordinary tools. + (make-fuse-statfs 0 0 0 0 0 4096 255 4096)) + --- a/tests/test-access.ss +++ b/tests/test-access.ss @@ -2,6 +2,8 @@ ;;; Run with: jerbuild exec --libdirs lib tests/test-access.ss (import (jerboa prelude)) +(import (jerboa-fuse)) +(import (jerboa-fuse constants)) (import (jerboa-fuse access)) (define pass 0) @@ -60,6 +62,30 @@ (define sdirents (stealth-deny-readdir 1)) (test-assert "stealth readdir has 2 entries" (= (length sdirents) 2)) +;; === STATFS access gate (P1 #33c) === +(display "=== stealth statfs / STATFS gate ===") (newline) + +;; STATFS must NOT be always-allowed: a denied caller would otherwise learn +;; vault block counts. It is routed through the access gate instead. +(test-assert "FUSE-STATFS is not always-allowed (gated)" + (not (always-allowed-opcode? FUSE-STATFS))) +(test-assert "FUSE-INIT is still always-allowed" + (always-allowed-opcode? FUSE-INIT)) + +;; A denied caller's STATFS receives a generic empty statfs (no block counts). +(define sstat (stealth-deny-statfs)) +(test-assert "stealth statfs is a fuse-statfs" (fuse-statfs? sstat)) +(test-assert "stealth statfs leaks no block counts" + (and (zero? (fuse-statfs-blocks sstat)) + (zero? (fuse-statfs-bfree sstat)) + (zero? (fuse-statfs-bavail sstat)) + (zero? (fuse-statfs-files sstat)) + (zero? (fuse-statfs-ffree sstat)))) +(test-assert "stealth statfs keeps sane bsize/namelen/frsize" + (and (= (fuse-statfs-bsize sstat) 4096) + (= (fuse-statfs-namelen sstat) 255) + (= (fuse-statfs-frsize sstat) 4096))) + ;; Summary (newline) (display "PASS: ") (display pass) (newline)