Skip to content

withheld_blob_recipients_bounded drops its deadline for the phase whose cost the owner controls #291

Description

@beardthelion

Summary

withheld_blob_recipients_bounded (crates/gitlawb-node/src/git/visibility_pack.rs:717) accepts a timeout and applies it to only one of its two phases. blob_paths at :726 is deadline-bounded. The recipient loop that follows at :738-749 is not, and it is the phase whose cost the repo owner controls.

Present unchanged on main at visibility_pack.rs:368, where it runs from a bare spawn_blocking (api/repos.rs:1147) with no permit and no timeout at all. This is not a #174 follow-up; #174 only adds the concurrency cap around it.

Cost

O(withheld_pairs x candidate_dids x rules), quadratic in rule count and near-quadratic in reader-DID count, because candidates is the union of reader_dids across all rules while visibility_check is itself O(rules).

visibility_check is not cheap per iteration. Per rule per call, glob_matches (visibility.rs:48) does two nfc() collects into fresh Strings plus a format!, and specificity (visibility.rs:62) does a third, then the winning rule linearly scans reader_dids (visibility.rs:100). Roughly 1 microsecond per rule per call.

Measured on a release build, the loop extracted verbatim (pairs / rules / readers-per-rule):

shape time
1,000 / 10 / 20 1.95 s
10,000 / 10 / 20 19.7 s
20,000 / 5 / 30 16.1 s
1,000 / 1 / 3,200 38.7 s
1,000 / 320 / 5 496 s

Scaling confirmed by execution: linear in pairs (500/1k/2k/4k gives 0.96/1.95/3.94/7.96 s), quadratic in rules (4x rules gives 14.6x to 16.5x), near-quadratic in readers. The 19.7 s row is an ordinary private monorepo with a 200-person org, no hostility required. Extrapolating the quadratic fit, 1,000 rules with 5 readers on a 10,000-pair repo is on the order of hours; that one is extrapolation, not measurement.

Nothing caps the inputs

validate_path_glob (api/visibility.rs:43) checks glob shape only, never count or DID-list length. The schema is reader_dids TEXT NOT NULL (db/mod.rs:754), unbounded, with UNIQUE(repo_id, path_glob) so rule count is bounded only by distinct globs. The visibility route (server.rs:174-177) is not in a group that calls DefaultBodyLimit::disable(), so it keeps axum's 2 MB default, which still admits roughly 35,000 did:key entries in a single PUT, and rule count accumulates across repeated PUTs.

What it costs the node

On this branch the permit lives inside the spawn_blocking closure (api/repos.rs:838), deliberately, so the call pins one of 32 node-wide git_encrypt_semaphore slots (main.rs:402, default from max_concurrent_git_pushes) and one blocking thread at full CPU for the duration. That pool is disjoint from the read and write pools and defers rather than sheds, so this does not directly block pushes or reads; the cross-tenant cost is the burned core and the starved background walks of other repos. On main there is no permit, so the only bound is the thread pool itself.

Reachability

Owner-only. The single production writer of visibility rules is api/visibility.rs:114, behind require_owner (:28); every other set_visibility_rule call site is test code. No sync, announce, p2p, admin, or migration path writes the table. Worth stating plainly: "owner" here means any authenticated DID that created a repo, not a privileged operator, so this is cross-tenant CPU starvation available to any account, not an anonymous DoS.

Suggested direction

A deadline over the tail is the obvious fix but probably the wrong one alone, since abandoning the loop partway yields an incomplete recipient map. Capping the inputs is likely the real answer: a maximum rule count per repo and a maximum reader_dids length per rule, validated in set_visibility. Memoizing visibility_check per (path, did) would also collapse the quadratic term.

Out of scope, noted so it is on record

withheld_from_pairs (visibility_pack.rs:548) is O(pairs x rules), linear in both, measured two to three orders below the tail. replicable_objects_fail_closed is one hash probe per candidate. Both are non-findings.

Separately, the same O(pairs x rules) shape appears on the anonymously reachable /ipfs/{cid} path via allowed_blob_set_for_caller_bounded (api/ipfs.rs:521) at lower magnitude, overlapping #164. Not this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:nodegitlawb-node — the serving node and REST APIkind:bugDefect fix — wrong or unsafe behaviorsev:mediumDegraded but workaround existssubsystem:visibilityPath-scoped visibility and content withholding

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions