fix: default allow-other to #f, truncate leak, rename checks, bounds validation, zero passphrases

ober

a7d6c064bddc9c5e41ff238190a72ca9567452e8

diff --git a/src/jerboa-fuse.ss b/src/jerboa-fuse.ss
index c858d33..65d98ec 100644
--- a/src/jerboa-fuse.ss
+++ b/src/jerboa-fuse.ss
@@ -154,14 +154,14 @@
   (def (create-session ops mountpoint options)
     (let* ([fsname (get-option options 'fsname "chez-fuse")]
            [debug? (get-option options 'debug #f)]
-           [allow-other? (get-option options 'allow-other #t)]
+           [allow-other? (get-option options 'allow-other #f)]
            [fd (fuse-open-device)]
            [mtx (make-mutex)]
            [ac  (get-option options 'access-controller #f)]
            [session (make-fuse-session
                       fd mountpoint #f #f
                       FUSE-KERNEL-VERSION FUSE-KERNEL-MINOR-VERSION
-                      131072 131072  ;; max-write, max-readahead
+                      131072 131072  ;; max_write, max_readahead
                       ops
                       mtx            ;; dispatch mutex
                       #f             ;; thread handle
@@ -169,7 +169,12 @@
                       ac)]            ;; access controller
            [uid (fuse-current-uid)]
            [gid (fuse-current-gid)])
-      (fuse-mount! fd mountpoint fsname uid gid allow-other?)
+      (guard (exn
+              [else
+               (fuse-close-device fd)
+               (fuse-session-fd-set! session -1)
+               (raise exn)])
+        (fuse-mount! fd mountpoint fsname uid gid allow-other?))
       (fuse-session-mounted?-set! session #t)
       (fuse-session-running?-set! session #t)
       (when debug?
@@ -298,6 +303,9 @@
   ;; Opcode dispatch
   ;; ======================================================================
 
+  (def (handler-error-code result)
+    (and (integer? result) (negative? result) (- result)))
+
   (def (dispatch-opcode session ops opcode unique nodeid ctx buf off limit)
     (cond
 
@@ -415,11 +423,14 @@
        (let ([handler (get-op ops 'mkdir)])
          (if handler
            (let-values ([(mode umask) (decode-mkdir-in buf off)])
-             (let* ([name (extract-name buf (+ off 8) limit)]
-                    [result (handler nodeid name mode ctx)])
-               (if result
-                 (encode-entry-out unique result)
-                 (encode-error unique EIO))))
+              (let* ([name (extract-name buf (+ off 8) limit)]
+                     [result (handler nodeid name mode ctx)])
+                (cond
+                  [(handler-error-code result)
+                   => (lambda (e) (encode-error unique e))]
+                  [result
+                   (encode-entry-out unique result)]
+                  [else (encode-error unique EIO)])))
            (encode-error unique ENOSYS)))]
 
       ;; ---- FUSE_UNLINK ----
@@ -451,10 +462,13 @@
            (let ([newdir (decode-rename-in buf off)])
              (let-values ([(oldname newname)
                            (extract-two-names buf (+ off 8) limit)])
-               (let ([result (handler nodeid oldname newdir newname ctx)])
-                 (if result
-                   (encode-out-header unique 0 0)
-                   (encode-error unique EIO)))))
+                (let ([result (handler nodeid oldname newdir newname ctx)])
+                  (cond
+                    [(handler-error-code result)
+                     => (lambda (e) (encode-error unique e))]
+                    [result
+                     (encode-out-header unique 0 0)]
+                    [else (encode-error unique EIO)]))))
            (encode-error unique ENOSYS)))]
 
       ;; ---- FUSE_LINK ----
@@ -507,13 +521,15 @@
          (if handler
            (let-values ([(fh offset size write-flags)
                          (decode-write-in buf off)])
-             (let* ([data-off (+ off 40)]
-                    [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)))))
+             (let* ([data-off (+ off 40)])
+                (if (> (+ data-off size) limit)
+                  (encode-error unique EINVAL)
+                  (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)))]
 
       ;; ---- FUSE_STATFS ----
@@ -602,30 +618,33 @@
          (if handler
            (let-values ([(flags mode umask open-flags)
                          (decode-create-in buf off)])
-             (let* ([name (extract-name buf (+ off 16) limit)]
-                    [result (handler nodeid name mode flags ctx)])
-               (if result
-                 (let* ([entry (car result)]
-                        [fh-part (cdr result)]
-                        [fh (if (pair? fh-part) (car fh-part) fh-part)]
-                        [oflags (if (pair? fh-part) (cdr fh-part) 0)]
-                        [total (+ FUSE-OUT-HEADER-SIZE
-                                  FUSE-ENTRY-OUT-SIZE
-                                  FUSE-OPEN-OUT-SIZE)]
-                        [resp (make-bytevector total 0)]
-                        [entry-bv (encode-entry-out unique entry)])
-                   (bytevector-u32-native-set! resp 0 total)
-                   (bytevector-s32-native-set! resp 4 0)
-                   (bytevector-u64-native-set! resp 8 unique)
-                   (bytevector-copy! entry-bv FUSE-OUT-HEADER-SIZE
-                                     resp FUSE-OUT-HEADER-SIZE
-                                     FUSE-ENTRY-OUT-SIZE)
-                   (bytevector-u64-native-set! resp
-                     (+ FUSE-OUT-HEADER-SIZE FUSE-ENTRY-OUT-SIZE) fh)
-                   (bytevector-u32-native-set! resp
-                     (+ FUSE-OUT-HEADER-SIZE FUSE-ENTRY-OUT-SIZE 8) oflags)
-                   resp)
-                 (encode-error unique EIO))))
+              (let* ([name (extract-name buf (+ off 16) limit)]
+                     [result (handler nodeid name mode flags ctx)])
+                (cond
+                  [(handler-error-code result)
+                   => (lambda (e) (encode-error unique e))]
+                  [result
+                   (let* ([entry (car result)]
+                          [fh-part (cdr result)]
+                          [fh (if (pair? fh-part) (car fh-part) fh-part)]
+                          [oflags (if (pair? fh-part) (cdr fh-part) 0)]
+                          [total (+ FUSE-OUT-HEADER-SIZE
+                                    FUSE-ENTRY-OUT-SIZE
+                                    FUSE-OPEN-OUT-SIZE)]
+                          [resp (make-bytevector total 0)]
+                          [entry-bv (encode-entry-out unique entry)])
+                     (bytevector-u32-native-set! resp 0 total)
+                     (bytevector-s32-native-set! resp 4 0)
+                     (bytevector-u64-native-set! resp 8 unique)
+                     (bytevector-copy! entry-bv FUSE-OUT-HEADER-SIZE
+                                       resp FUSE-OUT-HEADER-SIZE
+                                       FUSE-ENTRY-OUT-SIZE)
+                     (bytevector-u64-native-set! resp
+                       (+ FUSE-OUT-HEADER-SIZE FUSE-ENTRY-OUT-SIZE) fh)
+                     (bytevector-u32-native-set! resp
+                       (+ FUSE-OUT-HEADER-SIZE FUSE-ENTRY-OUT-SIZE 8) oflags)
+                     resp)]
+                  [else (encode-error unique EIO)])))
            (encode-error unique ENOSYS)))]
 
       ;; ---- FUSE_INTERRUPT ----
diff --git a/src/jerboa-fuse/access.ss b/src/jerboa-fuse/access.ss
index 0891d96..cb24039 100644
--- a/src/jerboa-fuse/access.ss
+++ b/src/jerboa-fuse/access.ss
@@ -25,12 +25,14 @@
 
   (def c-getpid #f)
   (def c-getppid-of #f)
+  (def c-get-start-time #f)
   (def *bindings-ready?* #f)
 
   (def (ensure-access-bindings!)
     (when (and (ensure-mount-lib!) (not *bindings-ready?*))
       (set! c-getpid (c-lambda () int "jerboa_fuse_getpid"))
       (set! c-getppid-of (c-lambda (int) int "jerboa_fuse_getppid_of"))
+      (set! c-get-start-time (c-lambda (int) integer-64 "jerboa_fuse_get_start_time"))
       (set! *bindings-ready?* #t))
     (unless *bindings-ready?*
       (error 'jerboa-fuse/access "unable to load native process helpers")))
@@ -58,8 +60,9 @@
 
   ;; ---- Access controller ----
 
-  ;; Cache entry: (pid . expiry-time)
+  ;; Cache entry: pid → (start-time . expiry-time)
   ;; Trusted PIDs are cached for a short TTL to avoid repeated sysctl calls.
+  ;; The start-time guards against PID reuse within the TTL window.
   ;; Denied PIDs are NOT cached (process might become a child later, though
   ;; unlikely — and we want to re-check in case of PID reuse).
   (def CACHE-TTL 5)  ;; seconds
@@ -101,18 +104,23 @@
         [else
          (let* ([cache (access-controller-state-cache ac)]
                 [now   (time-second (current-time))]
-                [expiry (eq-hashtable-ref cache pid #f)])
+                [entry (eq-hashtable-ref cache pid #f)])
            (cond
-             ;; Cache hit, not expired
-             [(and expiry (> expiry now)) #t]
-             ;; Cache miss or expired — do the walk
+             ;; Cache hit, not expired, and start time matches (no PID reuse)
+             [(and entry
+                   (> (cdr entry) now)
+                   (let ([st (c-get-start-time pid)])
+                     (and (>= st 0) (= st (car entry)))))
+              #t]
+             ;; Cache miss, expired, or PID reused — do the walk
              [else
               (let ([trusted? (pid-is-descendant?
                                 pid
                                 (access-controller-state-owner-pid ac))])
                 (when trusted?
-                  ;; Cache positive result
-                  (eq-hashtable-set! cache pid (+ now CACHE-TTL)))
+                  (let ([st (c-get-start-time pid)])
+                    (eq-hashtable-set! cache pid
+                      (cons st (+ now CACHE-TTL)))))
                 trusted?)]))])))
 
   ;; ---- Stealth deny responses ----
diff --git a/src/jerboa-fuse/memfs.ss b/src/jerboa-fuse/memfs.ss
index b120a51..671032a 100644
--- a/src/jerboa-fuse/memfs.ss
+++ b/src/jerboa-fuse/memfs.ss
@@ -436,14 +436,50 @@
           (let* ([old-children (memfs-node-data old-parent)]
                  [ino (hashtable-ref old-children old-name #f)])
             (if ino
-              (let ([new-children (memfs-node-data new-parent)])
-                ;; Remove old entry that might exist at destination
-                (let ([existing (hashtable-ref new-children new-name #f)])
-                  (when existing
-                    (hashtable-delete! (memfs-state-inodes fs) existing)))
-                (hashtable-delete! old-children old-name)
-                (hashtable-set! new-children new-name ino)
-                #t)
+              (let ([src-node (get-node fs ino)]
+                    [new-children (memfs-node-data new-parent)])
+                (if (not src-node)
+                  #f
+                  (let ([existing (hashtable-ref new-children new-name #f)])
+                    (if existing
+                      (let ([dst-node (get-node fs existing)])
+                        (if (not dst-node)
+                          #f
+                          (cond
+                            [(and (eq? (memfs-node-type src-node) 'dir)
+                                  (not (eq? (memfs-node-type dst-node) 'dir)))
+                             #f]
+                            [(and (not (eq? (memfs-node-type src-node) 'dir))
+                                  (eq? (memfs-node-type dst-node) 'dir))
+                             #f]
+                            [(and (eq? (memfs-node-type dst-node) 'dir)
+                                  (> (hashtable-size (memfs-node-data dst-node)) 0))
+                             #f]
+                            [else
+                             (let ([nl (- (memfs-node-nlink dst-node) 1)])
+                               (memfs-node-nlink-set! dst-node nl)
+                               (when (<= nl 0)
+                                 (hashtable-delete! (memfs-state-inodes fs) existing)))
+                             (when (eq? (memfs-node-type dst-node) 'dir)
+                               (memfs-node-nlink-set! new-parent
+                                 (max 2 (- (memfs-node-nlink new-parent) 1))))
+                             (hashtable-delete! old-children old-name)
+                             (hashtable-set! new-children new-name ino)
+                             (when (eq? (memfs-node-type src-node) 'dir)
+                               (memfs-node-nlink-set! old-parent
+                                 (max 2 (- (memfs-node-nlink old-parent) 1)))
+                               (memfs-node-nlink-set! new-parent
+                                 (+ (memfs-node-nlink new-parent) 1)))
+                             #t])))
+                      (begin
+                        (hashtable-delete! old-children old-name)
+                        (hashtable-set! new-children new-name ino)
+                        (when (eq? (memfs-node-type src-node) 'dir)
+                          (memfs-node-nlink-set! old-parent
+                            (max 2 (- (memfs-node-nlink old-parent) 1)))
+                          (memfs-node-nlink-set! new-parent
+                            (+ (memfs-node-nlink new-parent) 1)))
+                        #t)))))
               #f))
           #f))))
 
diff --git a/src/jerboa-fuse/vault.ss b/src/jerboa-fuse/vault.ss
index d86b137..4049e83 100644
--- a/src/jerboa-fuse/vault.ss
+++ b/src/jerboa-fuse/vault.ss
@@ -331,7 +331,8 @@
         [(< new-size old-size)
          ;; Shrink: zero out tail of last block; free excess blocks
          (let* ([last-lidx (if (zero? new-size) -1 (quotient (- new-size 1) BLOCK-PAYLOAD))]
-                [last-boff (if (zero? new-size) 0 (remainder new-size BLOCK-PAYLOAD))])
+                [last-boff (if (zero? new-size) 0 (remainder new-size BLOCK-PAYLOAD))]
+                [max-lidx  (if (zero? old-size) -1 (quotient (- old-size 1) BLOCK-PAYLOAD))])
            ;; Zero tail of last partial block
            (when (and (>= last-lidx 0) (> last-boff 0))
              (let ([phys (resolve-data-block vault inode last-lidx)])
@@ -343,22 +344,23 @@
                          (bytevector-u8-set! pay i 0)
                          (loop (+ i 1))))
                      (blockstore-write-block! (vault-state-bs vault) phys pay))))))
-           ;; Free blocks beyond last-lidx
+           ;; Free blocks beyond last-lidx, iterating all indices (sparse-aware)
            (let loop ([i (+ last-lidx 1)])
-             (let ([phys (resolve-data-block vault inode i)])
-               (when phys
-                 (bitmap-free! vault phys)
-                 (if (< i DIRECT-BLOCKS)
-                   (vector-set! (vault-inode-direct inode) i VAULT-BLOCK-INVALID)
-                   (let ([ind-blk (vault-inode-indirect inode)])
-                     (when (not (= ind-blk VAULT-BLOCK-INVALID))
-                       (let ([ind-pay (blockstore-read-block (vault-state-bs vault) ind-blk)])
-                         (when ind-pay
-                           (let ([ptr-idx (- i DIRECT-BLOCKS)])
-                             (bv-set-u64le! ind-pay (* ptr-idx 8) VAULT-BLOCK-INVALID)
-                             (blockstore-write-block! (vault-state-bs vault) ind-blk ind-pay)))))))
-                 (loop (+ i 1))))))
-         (vault-inode-size-set! inode new-size)]
+             (when (<= i max-lidx)
+               (let ([phys (resolve-data-block vault inode i)])
+                 (when phys
+                   (bitmap-free! vault phys)
+                   (if (< i DIRECT-BLOCKS)
+                     (vector-set! (vault-inode-direct inode) i VAULT-BLOCK-INVALID)
+                     (let ([ind-blk (vault-inode-indirect inode)])
+                       (when (not (= ind-blk VAULT-BLOCK-INVALID))
+                         (let ([ind-pay (blockstore-read-block (vault-state-bs vault) ind-blk)])
+                           (when ind-pay
+                             (let ([ptr-idx (- i DIRECT-BLOCKS)])
+                               (bv-set-u64le! ind-pay (* ptr-idx 8) VAULT-BLOCK-INVALID)
+                               (blockstore-write-block! (vault-state-bs vault) ind-blk ind-pay)))))))))
+               (loop (+ i 1))))
+           (vault-inode-size-set! inode new-size))]
         [else
          ;; Extend: just update size (data blocks will read as zeros)
          (vault-inode-size-set! inode new-size)])
@@ -496,7 +498,8 @@
            [salt          (vault-rand-bytes 32)]
            [master-key    (vault-rand-bytes VAULT-KEY-LEN)]
            ;; Derive passphrase key
-           [pk            (vault-pbkdf2 pass-bv salt KDF-ITERATIONS VAULT-KEY-LEN)]
+            [pk            (vault-pbkdf2 pass-bv salt KDF-ITERATIONS VAULT-KEY-LEN)]
+            [_             (bytevector-fill! pass-bv 0)]
            ;; Encrypt master key
            [mk-enc        (vault-encrypt-small pk master-key)]
            ;; Encrypt superblock block number (always 0)
@@ -566,6 +569,7 @@
                     (decode-vault-header hdr-bv)])
         ;; Derive passphrase key
         (let ([pk (vault-pbkdf2 pass-bv salt kdf-iter VAULT-KEY-LEN)])
+          (bytevector-fill! pass-bv 0)
           ;; Decrypt master key
           (let ([master-key (vault-decrypt-small pk mk-enc)])
             (unless master-key
@@ -653,7 +657,8 @@
         (let* ([pass-bv (if (string? passphrase) (string->utf8 passphrase) passphrase)])
           (let-values ([(_magic _ver _blksz _total salt kdf-iter mk-enc sb-enc)
                         (decode-vault-header hdr-bv)])
-            (let ([pk (vault-pbkdf2 pass-bv salt kdf-iter VAULT-KEY-LEN)])
+        (let ([pk (vault-pbkdf2 pass-bv salt kdf-iter VAULT-KEY-LEN)])
+          (bytevector-fill! pass-bv 0)
               (let ([master-key (vault-decrypt-small pk mk-enc)])
                 (bytevector-fill! pk 0)
                 (unless master-key
@@ -763,23 +768,27 @@
         (let ([parent (read-inode vault parent-ino)])
           (and parent
                (= (vault-inode-type parent) INODE-TYPE-DIR)
-               (let ([new-blk (bitmap-alloc! vault)])
-                 (and new-blk
-                      (let* ([now  (time-second (current-time))]
-                             [uid  (fuse-context-uid ctx)]
-                             [gid  (fuse-context-gid ctx)]
-                             [inode (make-vault-inode
-                                      new-blk INODE-TYPE-FILE
-                                      (bitwise-ior S-IFREG (bitwise-and mode #o7777))
-                                      uid gid 0 now now now 1
-                                      (string->utf8 name)
-                                      (make-vector DIRECT-BLOCKS VAULT-BLOCK-INVALID)
-                                      VAULT-BLOCK-INVALID)])
-                        (write-inode! vault inode)
-                        (dir-add! vault parent-ino parent new-blk name)
-                        (write-inode! vault parent)
-                        (let ([fh (alloc-fh! vault new-blk)])
-                          (cons (inode->fuse-entry inode) fh))))))))))
+               (if (> (bytevector-length (string->utf8 name)) VAULT-MAX-FILENAME)
+                 (- ENAMETOOLONG)
+                 (if (dir-lookup vault parent name)
+                   (- EEXIST)
+                   (let ([new-blk (bitmap-alloc! vault)])
+                     (and new-blk
+                          (let* ([now  (time-second (current-time))]
+                                 [uid  (fuse-context-uid ctx)]
+                                 [gid  (fuse-context-gid ctx)]
+                                 [inode (make-vault-inode
+                                          new-blk INODE-TYPE-FILE
+                                          (bitwise-ior S-IFREG (bitwise-and mode #o7777))
+                                          uid gid 0 now now now 1
+                                          (string->utf8 name)
+                                          (make-vector DIRECT-BLOCKS VAULT-BLOCK-INVALID)
+                                          VAULT-BLOCK-INVALID)])
+                            (write-inode! vault inode)
+                            (dir-add! vault parent-ino parent new-blk name)
+                            (write-inode! vault parent)
+                            (let ([fh (alloc-fh! vault new-blk)])
+                              (cons (inode->fuse-entry inode) fh))))))))))))
 
   (def (make-vault-mkdir vault)
     (lambda (parent-ino name mode ctx)
@@ -787,23 +796,27 @@
         (let ([parent (read-inode vault parent-ino)])
           (and parent
                (= (vault-inode-type parent) INODE-TYPE-DIR)
-               (let ([new-blk (bitmap-alloc! vault)])
-                 (and new-blk
-                      (let* ([now  (time-second (current-time))]
-                             [uid  (fuse-context-uid ctx)]
-                             [gid  (fuse-context-gid ctx)]
-                             [inode (make-vault-inode
-                                      new-blk INODE-TYPE-DIR
-                                      (bitwise-ior S-IFDIR (bitwise-and mode #o7777))
-                                      uid gid 0 now now now 2
-                                      (string->utf8 name)
-                                      (make-vector DIRECT-BLOCKS VAULT-BLOCK-INVALID)
-                                      VAULT-BLOCK-INVALID)])
-                        (write-inode! vault inode)
-                        (dir-add! vault parent-ino parent new-blk name)
-                        (vault-inode-nlink-set! parent (+ (vault-inode-nlink parent) 1))
-                        (write-inode! vault parent)
-                        (inode->fuse-entry inode)))))))))
+               (if (> (bytevector-length (string->utf8 name)) VAULT-MAX-FILENAME)
+                 (- ENAMETOOLONG)
+                 (if (dir-lookup vault parent name)
+                   (- EEXIST)
+                   (let ([new-blk (bitmap-alloc! vault)])
+                     (and new-blk
+                          (let* ([now  (time-second (current-time))]
+                                 [uid  (fuse-context-uid ctx)]
+                                 [gid  (fuse-context-gid ctx)]
+                                 [inode (make-vault-inode
+                                          new-blk INODE-TYPE-DIR
+                                          (bitwise-ior S-IFDIR (bitwise-and mode #o7777))
+                                          uid gid 0 now now now 2
+                                          (string->utf8 name)
+                                          (make-vector DIRECT-BLOCKS VAULT-BLOCK-INVALID)
+                                          VAULT-BLOCK-INVALID)])
+                            (write-inode! vault inode)
+                            (dir-add! vault parent-ino parent new-blk name)
+                            (vault-inode-nlink-set! parent (+ (vault-inode-nlink parent) 1))
+                            (write-inode! vault parent)
+                            (inode->fuse-entry inode)))))))))))
 
   (def (make-vault-unlink vault)
     (lambda (parent-ino name ctx)
@@ -858,20 +871,54 @@
                (= (vault-inode-type new-parent) INODE-TYPE-DIR)
                (let ([child-blk (dir-lookup vault old-parent old-name)])
                  (and child-blk
-                      (begin
-                        ;; Remove from old parent
-                        (dir-remove! vault old-parent old-name)
-                        ;; If destination exists, remove it
-                        (let ([existing (dir-lookup vault new-parent new-name)])
-                          (when existing
-                            (let ([ex-inode (read-inode vault existing)])
-                              (when ex-inode
-                                (free-inode-blocks! vault ex-inode)
-                                (flush-bitmap! vault)))))
-                        ;; Add to new parent
-                        (dir-add! vault new-parent-ino new-parent child-blk new-name)
-                        (write-inode! vault new-parent)
-                        #t))))))))
+                      (let ([child-inode (read-inode vault child-blk)])
+                        (and child-inode
+                             (let ([existing (dir-lookup vault new-parent new-name)])
+                               (if existing
+                                 (let ([ex-inode (read-inode vault existing)])
+                                   (and ex-inode
+                                        (cond
+                                          [(and (= (vault-inode-type child-inode) INODE-TYPE-DIR)
+                                                (not (= (vault-inode-type ex-inode) INODE-TYPE-DIR)))
+                                           #f]
+                                          [(and (not (= (vault-inode-type child-inode) INODE-TYPE-DIR))
+                                                (= (vault-inode-type ex-inode) INODE-TYPE-DIR))
+                                           #f]
+                                          [(and (= (vault-inode-type ex-inode) INODE-TYPE-DIR)
+                                                (not (dir-empty? vault ex-inode)))
+                                           #f]
+                                          [else
+                                           (if (> (vault-inode-nlink ex-inode) 1)
+                                             (vault-inode-nlink-set! ex-inode
+                                               (- (vault-inode-nlink ex-inode) 1))
+                                             (begin
+                                               (free-inode-blocks! vault ex-inode)
+                                               (flush-bitmap! vault)))
+                                           (when (= (vault-inode-type ex-inode) INODE-TYPE-DIR)
+                                             (vault-inode-nlink-set! new-parent
+                                               (max 2 (- (vault-inode-nlink new-parent) 1))))
+                                           (dir-remove! vault new-parent new-name)
+                                           (dir-remove! vault old-parent old-name)
+                                           (dir-add! vault new-parent-ino new-parent child-blk new-name)
+                                           (when (= (vault-inode-type child-inode) INODE-TYPE-DIR)
+                                             (vault-inode-nlink-set! old-parent
+                                               (max 2 (- (vault-inode-nlink old-parent) 1)))
+                                             (vault-inode-nlink-set! new-parent
+                                               (+ (vault-inode-nlink new-parent) 1)))
+                                           (write-inode! vault old-parent)
+                                           (write-inode! vault new-parent)
+                                           #t])))
+                                 (begin
+                                   (dir-remove! vault old-parent old-name)
+                                   (dir-add! vault new-parent-ino new-parent child-blk new-name)
+                                   (when (= (vault-inode-type child-inode) INODE-TYPE-DIR)
+                                     (vault-inode-nlink-set! old-parent
+                                       (max 2 (- (vault-inode-nlink old-parent) 1)))
+                                     (vault-inode-nlink-set! new-parent
+                                       (+ (vault-inode-nlink new-parent) 1)))
+                                   (write-inode! vault old-parent)
+                                   (write-inode! vault new-parent)
+                                   #t))))))))))))
 
   (def (make-vault-setattr vault)
     (lambda (ino valid fh size atime mtime ctime atimensec mtimensec ctimensec mode uid gid ctx)
diff --git a/src/jerboa-fuse/vault/blockstore.ss b/src/jerboa-fuse/vault/blockstore.ss
index b77fa01..579986a 100644
--- a/src/jerboa-fuse/vault/blockstore.ss
+++ b/src/jerboa-fuse/vault/blockstore.ss
@@ -246,24 +246,26 @@
   ;; of the crypto operation. The temporary bytevector is zeroed afterward.
 
   (def (blockstore-read-block bs block-num)
-    ;; Returns decrypted BLOCK-PAYLOAD-byte bv, or #f on auth failure / I/O error.
+    ;; Returns decrypted BLOCK-PAYLOAD-byte bv, or #f on I/O error / bad block.
+    ;; Raises an error if no master key is set (auth failure / locked vault).
     (with-mutex (blockstore-state-mutex bs)
       (ensure-block-num 'blockstore-read-block bs block-num)
       (let ([sk (blockstore-state-master-key bs)])
-        (and sk (secure-key? sk) (secure-key-live? sk)
-             (let* ([raw-bv (make-bytevector BLOCK-SIZE 0)]
-                    [offset (block-offset block-num)])
-               (raw-read! bs offset raw-bv)
-               (call-with-secure-key sk
-                 (lambda (mk-bv)
-                   (let ([bk #f])
-                     (dynamic-wind
-                       (lambda () (void))
-                       (lambda ()
-                         (set! bk (vault-block-key mk-bv block-num))
-                         (vault-decrypt-block bk raw-bv))
-                       (lambda ()
-                         (when bk (bytevector-fill! bk 0))))))))))))
+        (unless (and sk (secure-key? sk) (secure-key-live? sk))
+          (error 'blockstore-read-block "no master key set (vault locked or auth failure)"))
+        (let* ([raw-bv (make-bytevector BLOCK-SIZE 0)]
+               [offset (block-offset block-num)])
+          (raw-read! bs offset raw-bv)
+          (call-with-secure-key sk
+            (lambda (mk-bv)
+              (let ([bk #f])
+                (dynamic-wind
+                  (lambda () (void))
+                  (lambda ()
+                    (set! bk (vault-block-key mk-bv block-num))
+                    (vault-decrypt-block bk raw-bv))
+                  (lambda ()
+                    (when bk (bytevector-fill! bk 0)))))))))))
 
   (def (blockstore-write-block! bs block-num payload-bv)
     ;; Encrypts payload and writes to disk. payload-bv must be BLOCK-PAYLOAD bytes.
diff --git a/src/mount_helper.c b/src/mount_helper.c
index 8f87965..2d9c90c 100644
--- a/src/mount_helper.c
+++ b/src/mount_helper.c
@@ -28,6 +28,7 @@
  *   Process tree inspection:
  *     int   jerboa_fuse_getpid(void)
  *     int   jerboa_fuse_getppid_of(int pid)
+ *     int64_t jerboa_fuse_get_start_time(int pid)
  */
 
 #include <errno.h>
@@ -448,6 +449,15 @@ int jerboa_fuse_getppid_of(int pid) {
     return (int)kp.ki_ppid;
 }
 
+int64_t jerboa_fuse_get_start_time(int pid) {
+    struct kinfo_proc kp;
+    size_t len = sizeof(kp);
+    int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid };
+    if (sysctl(mib, 4, &kp, &len, NULL, 0) < 0) return -1;
+    if (len == 0) return -1;
+    return (int64_t)kp.ki_start.tv_sec;
+}
+
 #elif defined(LINUX)
 
 /* Get parent PID by reading /proc/<pid>/stat. Returns -1 on error. */
@@ -475,6 +485,34 @@ int jerboa_fuse_getppid_of(int pid) {
     return ppid;
 }
 
+int64_t jerboa_fuse_get_start_time(int pid) {
+    if (pid <= 0) {
+        errno = EINVAL;
+        return -1;
+    }
+    char path[64];
+    snprintf(path, sizeof(path), "/proc/%d/stat", pid);
+    FILE *f = fopen(path, "r");
+    if (!f) return -1;
+    char buf[512];
+    size_t n = fread(buf, 1, sizeof(buf) - 1, f);
+    fclose(f);
+    if (n == 0) return -1;
+    buf[n] = '\0';
+    char *p = strrchr(buf, ')');
+    if (!p) return -1;
+    /* Fields after ')': state(3) ppid(4) pgrp(5) session(6) tty_nr(7)
+       tpgid(8) flags(9) minflt(10) cminflt(11) majflt(12) cmajflt(13)
+       utime(14) stime(15) cutime(16) cstime(17) priority(18) nice(19)
+       num_threads(20) itrealvalue(21) starttime(22) */
+    long long starttime = -1;
+    if (sscanf(p + 2,
+               "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u "
+               "%*u %*u %*d %*d %*d %*d %*d %*d %lld",
+               &starttime) != 1) return -1;
+    return (int64_t)starttime;
+}
+
 #elif defined(DARWIN)
 
 int jerboa_fuse_getppid_of(int pid) {
@@ -488,6 +526,17 @@ int jerboa_fuse_getppid_of(int pid) {
     return (int)info.pbi_ppid;
 }
 
+int64_t jerboa_fuse_get_start_time(int pid) {
+    if (pid <= 0) {
+        errno = EINVAL;
+        return -1;
+    }
+    struct proc_bsdinfo info;
+    int ret = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, sizeof(info));
+    if (ret <= 0) return -1;
+    return (int64_t)info.pbi_start_tvsec;
+}
+
 #else
 
 int jerboa_fuse_getppid_of(int pid) {
@@ -495,6 +544,11 @@ int jerboa_fuse_getppid_of(int pid) {
     return -1;
 }
 
+int64_t jerboa_fuse_get_start_time(int pid) {
+    (void)pid;
+    return -1;
+}
+
 #endif
 
 /* ====================================================================