Where: packages/safe-fs/src/fs/quota/index.ts — assertDelta() calls usedBytes() (lines 19-53), which walks the entire filesystem tree (readdir + lstat per entry, recursive from /) on every writeFile/appendFile/copyFile/truncate/link/symlink/writeStream chunk. All mutations are additionally serialized through a single promise queue (mutate, lines 73-78).
PoC (measured, Node 22):
const fs = withFileSystemQuota(new MemoryFileSystem(), { maxBytes: 1 << 30 });
// create N files, then append 1 byte to each:
// N=500 → append phase 144 ms
// N=1000 → 615 ms
// N=2000 → 2 280 ms
// N=4000 → 15 211 ms (clean O(n²))
Each 1-byte append costs a full tree walk: O(entries) readdir+lstat per write ⇒ O(entries²) for N writes. Extrapolated: 20k files → ~6 min of serialized FS time for 20 KB of writes. On remote backends (S3/WebDAV) each walk is N HTTP round-trips — a single seq 1 20000 | xargs touch-seeded tree turns every subsequent write into thousands of billed subrequests, and the serialized mutation queue means one tenant's write stalls every other tenant sharing the wrapped FS.
Impact: (d) — CPU/DoS on Node deployments and subrequest/billing amplification on S3/WebDAV-backed ones, in the exact wrapper a host would deploy to prevent disk-fill abuse (v1 #11 remediation pattern). The shell's own write path (H5) compounds it.
Fix: maintain a running byte counter instead of re-walking: initialize once (or lazily), adjust by the known delta per mutation (size delta for writes/truncates, source size for copies/links), invalidate/rescan only on operations whose delta can't be known locally. Hardlink aliasing can be handled by charging max(0, delta) per directory entry sharing the inode without a global walk, or by tracking inode→bytes in a WeakMap keyed by identity scope. If a full walk is ever unavoidable, cache it with dirty-flag invalidation rather than per-write.
Where:
packages/safe-fs/src/fs/quota/index.ts—assertDelta()callsusedBytes()(lines 19-53), which walks the entire filesystem tree (readdir+lstatper entry, recursive from/) on everywriteFile/appendFile/copyFile/truncate/link/symlink/writeStreamchunk. All mutations are additionally serialized through a single promise queue (mutate, lines 73-78).PoC (measured, Node 22):
Each 1-byte append costs a full tree walk: O(entries)
readdir+lstatper write ⇒ O(entries²) for N writes. Extrapolated: 20k files → ~6 min of serialized FS time for 20 KB of writes. On remote backends (S3/WebDAV) each walk is N HTTP round-trips — a singleseq 1 20000 | xargs touch-seeded tree turns every subsequent write into thousands of billed subrequests, and the serialized mutation queue means one tenant's write stalls every other tenant sharing the wrapped FS.Impact: (d) — CPU/DoS on Node deployments and subrequest/billing amplification on S3/WebDAV-backed ones, in the exact wrapper a host would deploy to prevent disk-fill abuse (v1 #11 remediation pattern). The shell's own write path (H5) compounds it.
Fix: maintain a running byte counter instead of re-walking: initialize once (or lazily), adjust by the known delta per mutation (size delta for writes/truncates, source size for copies/links), invalidate/rescan only on operations whose delta can't be known locally. Hardlink aliasing can be handled by charging
max(0, delta)per directory entry sharing the inode without a global walk, or by tracking inode→bytes in a WeakMap keyed by identity scope. If a full walk is ever unavoidable, cache it with dirty-flag invalidation rather than per-write.