vault: enforce discretionary access control (P1 #33b)

ober

537fe9d9ed66463fcdfd42feab45a7576c8e39bb

diff --git a/lib/jerboa-fuse.sls b/lib/jerboa-fuse.sls
index a0f1548..44c2a78 100644
--- 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)])
diff --git a/lib/jerboa-fuse/vault.sls b/lib/jerboa-fuse/vault.sls
index 0429d91..3b9fd14 100644
--- 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)
diff --git a/src/jerboa-fuse.ss b/src/jerboa-fuse.ss
index 65d98ec..f22a92f 100644
--- 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)
diff --git a/src/jerboa-fuse/vault.ss b/src/jerboa-fuse/vault.ss
index 4049e83..002742f 100644
--- 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)
diff --git a/tests/test-vault.ss b/tests/test-vault.ss
index 1b968db..86e05c4 100644
--- 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)