vault: enforce discretionary access control (P1 #33b)
ober
537fe9d9ed66463fcdfd42feab45a7576c8e39bb
--- a/lib/jerboa-fuse.sls +++ b/lib/jerboa-fuse.sls @@ -275,9 +275,11 @@ (let ([result (handler nodeid valid fh size atime mtime ctime atimensec mtimensec ctimensec mode uid gid ctx)]) - (if result - (encode-attr-out unique 1 0 result) - (encode-error unique EIO)))) + (cond + [(handler-error-code result) => + (lambda (e) (encode-error unique e))] + [result (encode-attr-out unique 1 0 result)] + [else (encode-error unique EIO)]))) (encode-error unique ENOSYS)))] [(= opcode FUSE-READLINK) (let ([handler (get-op ops 'readlink)]) @@ -378,17 +380,20 @@ (let-values ([(fh offset size read-flags) (decode-read-in buf off)]) (let ([data (handler nodeid fh size offset ctx)]) - (if data - (let* ([dlen (bytevector-length data)] - [total (+ FUSE-OUT-HEADER-SIZE dlen)] - [resp (make-bytevector total 0)]) - (bytevector-u32-native-set! resp 0 total) - (bytevector-s32-native-set! resp 4 0) - (bytevector-u64-native-set! resp 8 unique) - (bytevector-copy! data 0 resp - FUSE-OUT-HEADER-SIZE dlen) - resp) - (encode-error unique EIO)))) + (cond + [(handler-error-code data) => + (lambda (e) (encode-error unique e))] + [data + (let* ([dlen (bytevector-length data)] + [total (+ FUSE-OUT-HEADER-SIZE dlen)] + [resp (make-bytevector total 0)]) + (bytevector-u32-native-set! resp 0 total) + (bytevector-s32-native-set! resp 4 0) + (bytevector-u64-native-set! resp 8 unique) + (bytevector-copy! data 0 resp FUSE-OUT-HEADER-SIZE + dlen) + resp)] + [else (encode-error unique EIO)]))) (encode-error unique ENOSYS)))] [(= opcode FUSE-WRITE) (let ([handler (get-op ops 'write)]) @@ -402,9 +407,11 @@ (bytevector-copy! buf data-off data 0 size) (let ([written (handler nodeid fh data offset ctx)]) - (if written - (encode-write-out unique written) - (encode-error unique EIO))))))) + (cond + [(handler-error-code written) => + (lambda (e) (encode-error unique e))] + [written (encode-write-out unique written)] + [else (encode-error unique EIO)])))))) (encode-error unique ENOSYS)))] [(= opcode FUSE-STATFS) (let ([handler (get-op ops 'statfs)]) --- a/lib/jerboa-fuse/vault.sls +++ b/lib/jerboa-fuse/vault.sls @@ -645,6 +645,31 @@ #t))))))) (def (vault-locked? vault) (not (blockstore-key-live? (vault-state-bs vault)))) + (def (inode-access-ok? inode want ctx) + (let ([mode (vault-inode-mode inode)] + [uid (fuse-context-uid ctx)] + [gid (fuse-context-gid ctx)]) + (cond + [(zero? (bitwise-and want (bitwise-ior R-OK W-OK X-OK))) #t] + [(= uid 0) + (if (zero? (bitwise-and want X-OK)) + #t + (not (zero? + (bitwise-and + mode + (bitwise-ior S-IXUSR S-IXGRP S-IXOTH)))))] + [else + (let ([granted (cond + [(= uid (vault-inode-uid inode)) + (bitwise-and + (bitwise-arithmetic-shift-right mode 6) + 7)] + [(= gid (vault-inode-gid inode)) + (bitwise-and + (bitwise-arithmetic-shift-right mode 3) + 7)] + [else (bitwise-and mode 7)])]) + (= (bitwise-and granted want) want))]))) (def (make-vault-getattr vault) (lambda (ino ctx) (with-mutex (vault-state-mutex vault) @@ -718,16 +743,20 @@ (lambda (ino fh size offset ctx) (with-mutex (vault-state-mutex vault) (let ([inode (read-inode vault ino)]) - (and inode - (= (vault-inode-type inode) INODE-TYPE-FILE) - (file-read vault inode size offset)))))) + (cond + [(not inode) #f] + [(not (= (vault-inode-type inode) INODE-TYPE-FILE)) #f] + [(not (inode-access-ok? inode R-OK ctx)) (- EACCES)] + [else (file-read vault inode size offset)]))))) (def (make-vault-write vault) (lambda (ino fh data offset ctx) (with-mutex (vault-state-mutex vault) (let ([inode (read-inode vault ino)]) - (and inode - (= (vault-inode-type inode) INODE-TYPE-FILE) - (file-write! vault ino inode data offset)))))) + (cond + [(not inode) #f] + [(not (= (vault-inode-type inode) INODE-TYPE-FILE)) #f] + [(not (inode-access-ok? inode W-OK ctx)) (- EACCES)] + [else (file-write! vault ino inode data offset)]))))) (def (make-vault-create vault) (lambda (parent-ino name mode flags ctx) (with-mutex (vault-state-mutex vault) @@ -971,40 +1000,74 @@ mtimensec ctimensec mode uid gid ctx) (with-mutex (vault-state-mutex vault) (let ([inode (read-inode vault ino)]) - (when inode - (when (not (zero? (bitwise-and valid FATTR-MODE))) - (vault-inode-mode-set! - inode - (bitwise-ior - (bitwise-and - (vault-inode-mode inode) - (bitwise-not 4095)) - (bitwise-and mode 4095)))) - (when (not (zero? (bitwise-and valid FATTR-UID))) - (vault-inode-uid-set! inode uid)) - (when (not (zero? (bitwise-and valid FATTR-GID))) - (vault-inode-gid-set! inode gid)) - (when (not (zero? (bitwise-and valid FATTR-SIZE))) - (file-truncate! vault ino inode size)) - (when (not (zero? (bitwise-and valid FATTR-ATIME))) - (vault-inode-atime-set! inode atime)) - (when (not (zero? (bitwise-and valid FATTR-MTIME))) - (vault-inode-mtime-set! inode mtime)) - (when (not (zero? (bitwise-and valid FATTR-ATIME-NOW))) - (vault-inode-atime-set! - inode - (time-second (current-time)))) - (when (not (zero? (bitwise-and valid FATTR-MTIME-NOW))) - (vault-inode-mtime-set! - inode - (time-second (current-time)))) - (vault-inode-ctime-set! inode (time-second (current-time))) - (write-inode! vault inode) - (inode->fuse-attr inode)))))) + (and inode + (let* ([caller-uid (fuse-context-uid ctx)] + [owner? (= caller-uid (vault-inode-uid inode))] + [root? (= caller-uid 0)]) + (cond + [(not (or owner? + root? + (inode-access-ok? inode W-OK ctx))) + (- EACCES)] + [(and (not root?) + (or (and (not (zero? + (bitwise-and valid FATTR-UID))) + (not (= uid (vault-inode-uid inode)))) + (and (not (zero? + (bitwise-and valid FATTR-GID))) + (not (= gid + (vault-inode-gid inode)))))) + (- EPERM)] + [else + (begin + (when (not (zero? (bitwise-and valid FATTR-MODE))) + (let ([bits (bitwise-and mode 4095)]) + (vault-inode-mode-set! + inode + (bitwise-ior + (bitwise-and + (vault-inode-mode inode) + (bitwise-not 4095)) + (if (or owner? root?) + bits + (bitwise-and + bits + (bitwise-not + (bitwise-ior + S-ISUID + S-ISGID)))))))) + (when (not (zero? (bitwise-and valid FATTR-UID))) + (vault-inode-uid-set! inode uid)) + (when (not (zero? (bitwise-and valid FATTR-GID))) + (vault-inode-gid-set! inode gid)) + (when (not (zero? (bitwise-and valid FATTR-SIZE))) + (file-truncate! vault ino inode size)) + (when (not (zero? + (bitwise-and valid FATTR-ATIME))) + (vault-inode-atime-set! inode atime)) + (when (not (zero? + (bitwise-and valid FATTR-MTIME))) + (vault-inode-mtime-set! inode mtime)) + (when (not (zero? + (bitwise-and valid FATTR-ATIME-NOW))) + (vault-inode-atime-set! + inode + (time-second (current-time)))) + (when (not (zero? + (bitwise-and valid FATTR-MTIME-NOW))) + (vault-inode-mtime-set! + inode + (time-second (current-time)))) + (vault-inode-ctime-set! + inode + (time-second (current-time))) + (write-inode! vault inode) + (inode->fuse-attr inode))]))))))) (def (make-vault-access vault) (lambda (ino mask ctx) (with-mutex (vault-state-mutex vault) - (let ([inode (read-inode vault ino)]) (if inode #t #f))))) + (let ([inode (read-inode vault ino)]) + (and inode (inode-access-ok? inode mask ctx)))))) (def (make-vault-statfs vault) (lambda (ctx) (with-mutex (vault-state-mutex vault) --- a/src/jerboa-fuse.ss +++ b/src/jerboa-fuse.ss @@ -380,10 +380,13 @@ atime mtime ctime atimensec mtimensec ctimensec mode uid gid ctx)]) - (if result - (encode-attr-out unique 1 0 result) - (encode-error unique EIO)))) - (encode-error unique ENOSYS)))] + (cond + [(handler-error-code result) + => (lambda (e) (encode-error unique e))] + [result + (encode-attr-out unique 1 0 result)] + [else (encode-error unique EIO)]))) + (encode-error unique ENOSYS)))] ;; ---- FUSE_READLINK ---- [(= opcode FUSE-READLINK) @@ -502,17 +505,20 @@ (if handler (let-values ([(fh offset size read-flags) (decode-read-in buf off)]) - (let ([data (handler nodeid fh size offset ctx)]) - (if data - (let* ([dlen (bytevector-length data)] - [total (+ FUSE-OUT-HEADER-SIZE dlen)] - [resp (make-bytevector total 0)]) - (bytevector-u32-native-set! resp 0 total) - (bytevector-s32-native-set! resp 4 0) - (bytevector-u64-native-set! resp 8 unique) - (bytevector-copy! data 0 resp FUSE-OUT-HEADER-SIZE dlen) - resp) - (encode-error unique EIO)))) + (let ([data (handler nodeid fh size offset ctx)]) + (cond + [(handler-error-code data) + => (lambda (e) (encode-error unique e))] + [data + (let* ([dlen (bytevector-length data)] + [total (+ FUSE-OUT-HEADER-SIZE dlen)] + [resp (make-bytevector total 0)]) + (bytevector-u32-native-set! resp 0 total) + (bytevector-s32-native-set! resp 4 0) + (bytevector-u64-native-set! resp 8 unique) + (bytevector-copy! data 0 resp FUSE-OUT-HEADER-SIZE dlen) + resp)] + [else (encode-error unique EIO)]))) (encode-error unique ENOSYS)))] ;; ---- FUSE_WRITE ---- @@ -527,10 +533,12 @@ (let ([data (make-bytevector size)]) (bytevector-copy! buf data-off data 0 size) (let ([written (handler nodeid fh data offset ctx)]) - (if written - (encode-write-out unique written) - (encode-error unique EIO))))))) - (encode-error unique ENOSYS)))] + (cond + [(handler-error-code written) + => (lambda (e) (encode-error unique e))] + [written (encode-write-out unique written)] + [else (encode-error unique EIO)])))))) + (encode-error unique ENOSYS)))] ;; ---- FUSE_STATFS ---- [(= opcode FUSE-STATFS) --- a/src/jerboa-fuse/vault.ss +++ b/src/jerboa-fuse/vault.ss @@ -688,6 +688,34 @@ (not (blockstore-key-live? (vault-state-bs vault)))) ;; ====================================================================== + ;; Discretionary access control + ;; ====================================================================== + + ;; POSIX-style check of `want` (R-OK/W-OK/X-OK mask) against the inode mode + ;; for the caller uid/gid carried in the FUSE context. The kernel is treated + ;; as untrusted, so the daemon enforces modes itself rather than relying on + ;; default_permissions. + (def (inode-access-ok? inode want ctx) + (let ([mode (vault-inode-mode inode)] + [uid (fuse-context-uid ctx)] + [gid (fuse-context-gid ctx)]) + (cond + [(zero? (bitwise-and want (bitwise-ior R-OK W-OK X-OK))) #t] + [(= uid 0) + ;; Root reads/writes anything; execute needs at least one x bit. + (if (zero? (bitwise-and want X-OK)) + #t + (not (zero? (bitwise-and mode (bitwise-ior S-IXUSR S-IXGRP S-IXOTH)))))] + [else + (let ([granted (cond + [(= uid (vault-inode-uid inode)) + (bitwise-and (bitwise-arithmetic-shift-right mode 6) 7)] + [(= gid (vault-inode-gid inode)) + (bitwise-and (bitwise-arithmetic-shift-right mode 3) 7)] + [else (bitwise-and mode 7)])]) + (= (bitwise-and granted want) want))]))) + + ;; ====================================================================== ;; FUSE op implementations ;; ====================================================================== @@ -750,17 +778,21 @@ (lambda (ino fh size offset ctx) (with-mutex (vault-state-mutex vault) (let ([inode (read-inode vault ino)]) - (and inode - (= (vault-inode-type inode) INODE-TYPE-FILE) - (file-read vault inode size offset)))))) + (cond + [(not inode) #f] + [(not (= (vault-inode-type inode) INODE-TYPE-FILE)) #f] + [(not (inode-access-ok? inode R-OK ctx)) (- EACCES)] + [else (file-read vault inode size offset)]))))) (def (make-vault-write vault) (lambda (ino fh data offset ctx) (with-mutex (vault-state-mutex vault) (let ([inode (read-inode vault ino)]) - (and inode - (= (vault-inode-type inode) INODE-TYPE-FILE) - (file-write! vault ino inode data offset)))))) + (cond + [(not inode) #f] + [(not (= (vault-inode-type inode) INODE-TYPE-FILE)) #f] + [(not (inode-access-ok? inode W-OK ctx)) (- EACCES)] + [else (file-write! vault ino inode data offset)]))))) (def (make-vault-create vault) (lambda (parent-ino name mode flags ctx) @@ -924,34 +956,55 @@ (lambda (ino valid fh size atime mtime ctime atimensec mtimensec ctimensec mode uid gid ctx) (with-mutex (vault-state-mutex vault) (let ([inode (read-inode vault ino)]) - (when inode - (when (not (zero? (bitwise-and valid FATTR-MODE))) - (vault-inode-mode-set! inode - (bitwise-ior (bitwise-and (vault-inode-mode inode) (bitwise-not #o7777)) - (bitwise-and mode #o7777)))) - (when (not (zero? (bitwise-and valid FATTR-UID))) - (vault-inode-uid-set! inode uid)) - (when (not (zero? (bitwise-and valid FATTR-GID))) - (vault-inode-gid-set! inode gid)) - (when (not (zero? (bitwise-and valid FATTR-SIZE))) - (file-truncate! vault ino inode size)) - (when (not (zero? (bitwise-and valid FATTR-ATIME))) - (vault-inode-atime-set! inode atime)) - (when (not (zero? (bitwise-and valid FATTR-MTIME))) - (vault-inode-mtime-set! inode mtime)) - (when (not (zero? (bitwise-and valid FATTR-ATIME-NOW))) - (vault-inode-atime-set! inode (time-second (current-time)))) - (when (not (zero? (bitwise-and valid FATTR-MTIME-NOW))) - (vault-inode-mtime-set! inode (time-second (current-time)))) - (vault-inode-ctime-set! inode (time-second (current-time))) - (write-inode! vault inode) - (inode->fuse-attr inode)))))) + (and inode + (let* ([caller-uid (fuse-context-uid ctx)] + [owner? (= caller-uid (vault-inode-uid inode))] + [root? (= caller-uid 0)]) + (cond + ;; DAC gate: owner, root, or write access required to setattr. + [(not (or owner? root? (inode-access-ok? inode W-OK ctx))) + (- EACCES)] + ;; Ownership changes (chown/chgrp) require root. + [(and (not root?) + (or (and (not (zero? (bitwise-and valid FATTR-UID))) + (not (= uid (vault-inode-uid inode)))) + (and (not (zero? (bitwise-and valid FATTR-GID))) + (not (= gid (vault-inode-gid inode)))))) + (- EPERM)] + [else + (begin + (when (not (zero? (bitwise-and valid FATTR-MODE))) + (let ([bits (bitwise-and mode #o7777)]) + (vault-inode-mode-set! inode + (bitwise-ior + (bitwise-and (vault-inode-mode inode) (bitwise-not #o7777)) + ;; Strip setuid/setgid on a non-owner, non-root chmod. + (if (or owner? root?) bits + (bitwise-and bits + (bitwise-not (bitwise-ior S-ISUID S-ISGID)))))))) + (when (not (zero? (bitwise-and valid FATTR-UID))) + (vault-inode-uid-set! inode uid)) + (when (not (zero? (bitwise-and valid FATTR-GID))) + (vault-inode-gid-set! inode gid)) + (when (not (zero? (bitwise-and valid FATTR-SIZE))) + (file-truncate! vault ino inode size)) + (when (not (zero? (bitwise-and valid FATTR-ATIME))) + (vault-inode-atime-set! inode atime)) + (when (not (zero? (bitwise-and valid FATTR-MTIME))) + (vault-inode-mtime-set! inode mtime)) + (when (not (zero? (bitwise-and valid FATTR-ATIME-NOW))) + (vault-inode-atime-set! inode (time-second (current-time)))) + (when (not (zero? (bitwise-and valid FATTR-MTIME-NOW))) + (vault-inode-mtime-set! inode (time-second (current-time)))) + (vault-inode-ctime-set! inode (time-second (current-time))) + (write-inode! vault inode) + (inode->fuse-attr inode))]))))))) (def (make-vault-access vault) (lambda (ino mask ctx) (with-mutex (vault-state-mutex vault) (let ([inode (read-inode vault ino)]) - (if inode #t #f))))) + (and inode (inode-access-ok? inode mask ctx)))))) (def (make-vault-statfs vault) (lambda (ctx) --- a/tests/test-vault.ss +++ b/tests/test-vault.ss @@ -2,6 +2,7 @@ ;;; Run with: jerbuild exec --libdirs lib tests/test-vault.ss (import (jerboa prelude)) +(import (jerboa-fuse)) (import (jerboa-fuse vault)) (import (jerboa-fuse access)) (import (jerboa-fuse vault format)) @@ -126,6 +127,74 @@ (cleanup) +(display "=== vault discretionary access control ===") (newline) + +;; P1 #33b: make-vault-access / read / write / setattr must enforce mode bits +;; against the FUSE context uid/gid. A 0600 file owned by uid 1000 must deny +;; read/write/setattr to any other uid (EACCES); only root may chown (EPERM). +(define v-dac (vault-create! test-path "secret123" 256)) +(define ops-dac (vault->fuse-ops v-dac)) +(define (op-ref key) (eq-hashtable-ref ops-dac key #f)) + +(define ctx-owner (make-fuse-context 1000 1000 0)) +(define ctx-other (make-fuse-context 2000 2000 0)) +(define ctx-root (make-fuse-context 0 0 0)) + +(define create-result + ((op-ref 'create) FUSE-ROOT-ID "secret.txt" #o0600 0 ctx-owner)) +(test-assert "owner create of 0600 file succeeds" + (and (pair? create-result) (fuse-entry? (car create-result)))) +(define file-ino (fuse-entry-nodeid (car create-result))) +(define file-fh (cdr create-result)) + +(test-assert "non-owner read of 0600 file denied (EACCES)" + (= ((op-ref 'read) file-ino file-fh 16 0 ctx-other) (- EACCES))) +(test-assert "owner read of 0600 file succeeds" + (bytevector? ((op-ref 'read) file-ino file-fh 16 0 ctx-owner))) + +(test-assert "non-owner write to 0600 file denied (EACCES)" + (= ((op-ref 'write) file-ino file-fh (string->utf8 "x") 0 ctx-other) + (- EACCES))) +(test-assert "owner write to 0600 file succeeds" + (integer? ((op-ref 'write) file-ino file-fh (string->utf8 "data") 0 ctx-owner))) + +;; Non-owner setattr (chown to root) is denied at the DAC gate (EACCES). +(test-assert "non-owner setattr (chown to root) denied (EACCES)" + (= ((op-ref 'setattr) file-ino FATTR-UID file-fh + 0 0 0 0 0 0 0 0 0 0 ctx-other) + (- EACCES))) +;; Owner may setattr but cannot chown to another uid; only root can (EPERM). +(test-assert "owner chown to root denied (EPERM)" + (= ((op-ref 'setattr) file-ino FATTR-UID file-fh + 0 0 0 0 0 0 0 0 0 0 ctx-owner) + (- EPERM))) +;; Root may chown freely. +(test-assert "root chown succeeds" + (fuse-attr? + ((op-ref 'setattr) file-ino FATTR-UID file-fh + 0 0 0 0 0 0 0 0 1000 0 ctx-root))) + +(test-assert "non-owner access(R_OK) on 0600 file denied" + (not ((op-ref 'access) file-ino R-OK ctx-other))) +(test-assert "owner access(R_OK) granted" + ((op-ref 'access) file-ino R-OK ctx-owner)) + +;; setuid/setgid are stripped when a non-owner chmods a writable file. +(define world-result + ((op-ref 'create) FUSE-ROOT-ID "world.txt" #o0777 0 ctx-owner)) +(define world-ino (fuse-entry-nodeid (car world-result))) +(define world-fh (cdr world-result)) +(test-assert "non-owner chmod of 0777 file strips setuid/setgid" + (and (fuse-attr? + ((op-ref 'setattr) world-ino FATTR-MODE world-fh + 0 0 0 0 0 0 0 #o6755 0 0 ctx-other)) + (let ([m (fuse-attr-mode + ((op-ref 'getattr) world-ino ctx-other))]) + (= (bitwise-and m #o7777) #o0755)))) + +(vault-close! v-dac) +(cleanup) + (display "=== Summary ===") (newline) (display "PASS: ") (display pass) (newline) (display "FAIL: ") (display fail) (newline)