Skip to content

Commit 3e1e904

Browse files
authored
fix(node): close under-withholding via full ref scope and full-history classification (#42) (#84)
* fix(node): close under-withholding via full ref scope and reachable-only pin set blob_paths walked only refs/heads/* and refs/tags/* and skipped silently on a failed git ls-tree, so a blob reachable only through another namespace, or a ref that failed to traverse, could fall out of the withheld set and ship in cleartext. Walk every ref and fail closed on traversal error. The pin enumerators (ipfs_pin, pinata) used git cat-file --batch-all-objects, which includes unreachable/dangling objects that have no path and cannot be classified for withholding. Switch them to git rev-list --objects --all so the pin set matches the reachable graph blob_paths evaluates. * fix(node): include HEAD in the withheld-blob walk to close pin/serve asymmetry blob_paths classified objects from for-each-ref only, but the pin set (git rev-list --objects --all) and git upload-pack both also expose objects reachable from HEAD. A detached HEAD (or any HEAD pointing outside the ref-covered set) therefore put a blob into the pin/serve path that the withholding walk never classified — a latent cleartext leak of a withheld blob. Walk HEAD too (guarded for unborn HEAD on an empty repo) so the withheld set is a superset of every exposure path. Adds a regression test: a blob reachable only via a detached HEAD must still be withheld (fails without this change). * fix(node): classify blobs across full history, not just ref tips blob_paths walked only ref-tip trees (git ls-tree -r <ref>), so a blob that existed in an older commit and was deleted or rotated at the tip was never classified, while the pin set (git rev-list --objects --all) and upload-pack still expose it from history. That under-withheld the blob into the IPFS/Pinata replication paths in cleartext. Enumerate all reachable commits (git rev-list --all + HEAD for the detached case) and walk each commit tree, deduping (oid, path) pairs. ls-tree per commit is kept over rev-list --objects so every path a blob lives at is still represented for the per-path visibility check. A ref that resolves to a non-commit now fails closed explicitly, since rev-list --all silently skips it where the old per-ref ls-tree errored. Regression test: a secret blob deleted at the tip but reachable in history is withheld. * fix(node): share one history walk and fully peel ref types in withheld classifier Addresses two review findings on the under-withholding classifier: - withheld_blob_recipients walked blob_paths twice (once via withheld_blob_oids, once directly). Extract withheld_from_pairs so a single history walk feeds both the withheld set and the recipient map. - The ref-type fail-closed guard used for-each-ref %(*objecttype), which peels only one tag level and so misclassified a nested annotated tag of a commit as a non-commit, refusing the whole walk (over-withhold). List refnames and peel each fully with <ref>^{} through one cat-file --batch-check; reap the child even if the stdin write fails. Fail closed on a short read or any non-commit/missing target. Adds tests: annotated + nested-tag of a commit does not fail closed, annotated tag of a blob still fails closed, and a blob at both an allowed and a denied path is not withheld. * fix(node): drain cat-file stdout concurrently to close the peel deadlock The ref-type guard peeled every <ref>^{} through git cat-file --batch-check, writing all queries to stdin before draining stdout. On a repo with enough output the child's stdout pipe fills, it stops reading stdin, and write_all blocks forever — hanging every serve/pin that classifies the repo. Batching the queries does NOT bound this: cat-file echoes the full query on a '<query> missing' line, so output scales with refname length, and a few hundred long-named dangling refs already exceed the pipe buffer. Run the stdin write on a separate thread so the main thread drains stdout concurrently via wait_with_output, which reaps the child unconditionally before any error is surfaced; the writer is joined after the drain so it cannot deadlock, and a vanished stdin pipe becomes a broken-pipe write error. One pass, no batch bound, deadlock-free regardless of ref count or refname length. Extracted as assert_all_refs_are_commits with corrected doc attribution. Detect the missing-object line by its trailing status word. Adds a timeout-bounded regression test with 500 long-named dangling refs (>64 KiB of cat-file output) that fails rather than hangs if the deadlock returns. * fix(node): parse ls-tree -z raw paths and fail closed on non-UTF-8 to stop under-withholding Plain `git ls-tree -r` C-quotes any path with non-ASCII or special bytes (café.txt becomes "secret/caf\303\251.txt"), so the stored path no longer matched a visibility rule like /secret/** and the blob was served/pinned in cleartext. Switch blob_paths to `git ls-tree -rz` and split the NUL-delimited records, which emit paths raw; the TAB field separator survives -z so the per-record parse is otherwise unchanged. Parse strictly with str::from_utf8 and fail closed on invalid UTF-8 rather than from_utf8_lossy: a lossy decode would rewrite an invalid byte in a denied directory name to U+FFFD, and the mangled path would again miss its deny rule (the same under-withholding class one layer down). Tests: a non-ASCII path stays withheld (and the public blob still replicates), and a non-UTF-8 path fails closed.
1 parent 1960f6b commit 3e1e904

1 file changed

Lines changed: 773 additions & 26 deletions

File tree

0 commit comments

Comments
 (0)