Skip to content

safe-fs quota wrapper: O(tree) full-filesystem walk per write — O(n²) DoS, serialized across tenants #630

Description

@kamilio

Where: packages/safe-fs/src/fs/quota/index.tsassertDelta() 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is not working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions