Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6633d17
fix(node): bound REST blob reads
euxaristia Sep 7, 2026
95d295c
test(node): cover blob authorization denials.
euxaristia Sep 7, 2026
d824cfa
fix(node): Hide Git diagnostics from blob error responses.
euxaristia Sep 9, 2026
3d7032b
fix(node): close REST blob admission and error gaps
euxaristia Sep 9, 2026
fc2d043
fix(node): acquire blob permits pre-DB and cover route/fallback arms
euxaristia Sep 10, 2026
c63527c
fix(node): confirm blob absence against a readable store
euxaristia Sep 12, 2026
8977ed4
fix(node): bound REST blob response delivery
euxaristia Sep 12, 2026
e1fe66e
test(git): distinguish every bounded ref fallback
euxaristia Sep 12, 2026
4344b77
docs(node): describe the cat-file blob read path
euxaristia Sep 12, 2026
49aebb2
fix(node): prevent caching of authorized blob content
euxaristia Sep 12, 2026
c566455
fix(node): pin non-blob denial and map unreadable store to 503
euxaristia Sep 12, 2026
df5a283
style(repos): simplify blob semaphore permit assertion for clippy
euxaristia Sep 13, 2026
f0d0025
fix(git): classify unreadable store with ProbeError and tighten blob …
euxaristia Sep 13, 2026
eb1222d
test(node): assert full blob admission release after read errors
euxaristia Sep 14, 2026
c15acf5
test(git): require probe completion before reprobe budget denial
euxaristia Sep 14, 2026
77e9b76
test(node): make Windows regression fixtures portable
euxaristia Sep 14, 2026
5d238ca
fix(sync): use a supported Windows promisor blob filter
euxaristia Sep 14, 2026
2c7f60a
test(git): route write-then-exec fixtures through write_blob_probe_fi…
euxaristia Sep 15, 2026
2bb398e
test(sync): assert configured partialclonefilter and pin platform con…
euxaristia Sep 15, 2026
bbab52e
test(api,git): close write-fd window in exec'd fixtures
euxaristia Sep 17, 2026
ad003b3
fix(api): clamp authorize_repo_read with acquire timeout in get_blob
euxaristia Sep 18, 2026
a2c996a
docs(config): describe the blob authorization deadline
euxaristia Sep 20, 2026
5f588c1
test(api): bound the blob authorization timeout regression
euxaristia Sep 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,28 @@ GITLAWB_MAX_PACK_BYTES=2147483648
# rather than giving each stage a full budget: a walk that consumes it leaves the
# serve nothing and the clone gets a 504. Serving large path-scoped repos may
# need a higher value here than when each stage was budgeted separately.
# REST blob size probes and content reads also share this deadline and return 504 on timeout.
# Body delivery gets the same time allowance; expiry aborts the response and releases admission.
# Must be 1..=3153600000 (100 years): the node derives deadlines from this value,
# and a larger one cannot be represented. Default 600.
GITLAWB_GIT_SERVICE_TIMEOUT_SECS=600

# Max seconds the storage-ACQUISITION phase of a served git op may run before the
# request is shed with a 503, separate from the git-run timeout above. A
# concurrency permit is taken before this phase and GITLAWB_GIT_SERVICE_TIMEOUT_SECS
# only starts once git spawns, so without this a stalled backend (a hung Tigris
# HEAD/GET, or a hung pg advisory-lock iteration on push) pins the permit and drains
# the pool until every later request 503s. On expiry the permit is released
# request is shed with a 503, separate from the git-run timeout above. Each
# REST blob read also applies this deadline independently to its preceding
# PostgreSQL authorization wait. A concurrency permit is taken before this phase
# and GITLAWB_GIT_SERVICE_TIMEOUT_SECS only starts once git spawns, so without
# this a stalled backend (a hung Tigris HEAD/GET, or a hung pg advisory-lock
# iteration on push) pins the permit and drains the pool until every later
# request 503s. On expiry the permit is released
# (fail-closed). Kept separate because acquisition and git execution are distinct
# cost centers. Must be positive; set very large to effectively disable. Default 30.
GITLAWB_GIT_ACQUIRE_TIMEOUT_SECS=30

# Max concurrent git READ ops (upload-pack + the upload-pack info/refs
# advertisement) served at once, a global pool separate from the push pool below.
# Max concurrent git READ ops (upload-pack, its info/refs advertisement, and REST
# blob reads) served at once, a global pool separate from the push pool below.
# REST blobs also have a fixed four-response sub-pool and a 32 MiB per-response
# ceiling; their permits remain held until the response body finishes or disconnects.
# The anon receive-pack info/refs advertisement has its OWN pool (see below), not
# this one. Over-cap sheds a clean 503 + Retry-After. Anonymous reads draw from
# here, so pair it with GITLAWB_MAX_CONCURRENT_READS_PER_CALLER (below) so one
Expand Down Expand Up @@ -182,6 +188,7 @@ GITLAWB_MAX_CONCURRENT_PIN_TASKS=8
# collapses to one global cap. Set GITLAWB_TRUSTED_PROXY for per-client keying; a
# high-fanout caller (CI behind one NAT) then needs the operator to raise this.
# Default 16.
# REST blob downloads also acquire this per-caller read allowance.
GITLAWB_MAX_CONCURRENT_READS_PER_CALLER=16

# Two further per-source concurrency caps exist on the PUSH side but have NO
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ Or build from source:
cargo build --release -p gl -p git-remote-gitlawb -p gitlawb-node
```

Node promisor mirrors use a 10 GiB blob filter on Unix. Git for Windows requires
a filter below 4 GiB, so Windows mirrors use 4 GiB minus one byte; larger blobs
remain available through on-demand fetching.

Put these binaries on your `PATH`:

```txt
Expand Down Expand Up @@ -340,6 +344,11 @@ GET /{owner}/{repo}/info/refs
POST /{owner}/{repo}/git-upload-pack
```

REST blob reads serve file content only; directory and gitlink paths return 404.
Body delivery has its own allowance equal to `GITLAWB_GIT_SERVICE_TIMEOUT_SECS`.
If delivery exceeds it, the response is interrupted and its admission slots are released.
Blob responses use `Cache-Control: no-store` because they may contain private content.

Signed write routes include:

```txt
Expand Down Expand Up @@ -396,9 +405,9 @@ Important node settings:
| `GITLAWB_ENFORCE_OWNER_PUSH` | Require the authenticated pusher to be the repo owner on `git-receive-pack`. **Defaults to `true`.** A `did:key` signature is authentication, not authorization — anyone can mint a key and sign — so with this off every signed caller may push to every repository, private ones included. Delegated and CI keys count as non-owners: a UCAN `git/push` capability is verified but not yet honored for authorization, so they cannot push while this is on. Set `false` only for a rolling upgrade; see [`docs/RUN-A-NODE.md`](docs/RUN-A-NODE.md). |
| `GITLAWB_AUTO_SYNC` | Enable automatic sync from known peers. |
| `GITLAWB_MAX_PACK_BYTES` | Max git pack body size for smart-HTTP routes. |
| `GITLAWB_GIT_SERVICE_TIMEOUT_SECS` | Max seconds a served git upload-pack, receive-pack, or `info/refs` advertisement may run before it is aborted (504). Default 600. Also bounds the withheld-blob classification walk (on both the upload-pack serve and receive-pack replication paths) and the push-side pin-candidate discovery (`rev-list` / `cat-file`), each reaped via process-group teardown at the deadline. On the path-scoped upload-pack path the classification walk and the pack serve share ONE deadline, so this value bounds their combined duration rather than granting each stage a full budget: a walk that consumes it leaves the serve nothing and the clone gets a 504. Serving large path-scoped repos may therefore need a higher value than they did when each stage was budgeted separately. Accepted range is 1 to 3153600000 (100 years), since the node derives deadlines from this value and a larger one cannot be represented. |
| `GITLAWB_GIT_ACQUIRE_TIMEOUT_SECS` | Max seconds the storage-acquisition phase (Tigris HEAD/GET, push advisory-lock) of a served git op may run before the request is shed with a 503, separate from the git-run timeout. The concurrency permit is released on expiry so a stalled backend cannot pin the pool. Default 30. |
| `GITLAWB_MAX_CONCURRENT_GIT_OPS` | Max concurrent served git READ ops (upload-pack and its `info/refs` advertisement) across all callers; over-cap sheds a 503 + Retry-After. Anonymous reads draw from this pool, so pair it with `GITLAWB_MAX_CONCURRENT_READS_PER_CALLER`. Pushes and the receive-pack advertisement have their own pools, so a read flood cannot shed an authenticated push. Default 128. |
| `GITLAWB_GIT_SERVICE_TIMEOUT_SECS` | Max seconds a served git upload-pack, receive-pack, `info/refs` advertisement, or REST blob read may run before it is aborted (504). Default 600. Also bounds the withheld-blob classification walk (on both the upload-pack serve and receive-pack replication paths) and the push-side pin-candidate discovery (`rev-list` / `cat-file`), each reaped via process-group teardown at the deadline. On the path-scoped upload-pack path the classification walk and the pack serve share ONE deadline, so this value bounds their combined duration rather than granting each stage a full budget: a walk that consumes it leaves the serve nothing and the clone gets a 504. Serving large path-scoped repos may therefore need a higher value than they did when each stage was budgeted separately. Accepted range is 1 to 3153600000 (100 years), since the node derives deadlines from this value and a larger one cannot be represented. |
| `GITLAWB_GIT_ACQUIRE_TIMEOUT_SECS` | Max seconds the storage-acquisition phase (Tigris HEAD/GET, push advisory-lock) of a served git op may run before the request is shed with a 503, separate from the git-run timeout. REST blob reads also apply this deadline independently to the preceding PostgreSQL authorization wait. The concurrency permit is released on expiry so a stalled backend cannot pin the pool. Default 30. |
| `GITLAWB_MAX_CONCURRENT_GIT_OPS` | Max concurrent served git READ ops (upload-pack, its `info/refs` advertisement, and REST blob reads) across all callers; over-cap sheds a 503 + Retry-After. Anonymous reads draw from this pool, so pair it with `GITLAWB_MAX_CONCURRENT_READS_PER_CALLER`. Pushes and the receive-pack advertisement have their own pools, so a read flood cannot shed an authenticated push. Default 128. REST blob responses are limited to 32 MiB each and a dedicated four-request pool holds admission through body delivery, bounding retained blob data to 128 MiB. |
| `GITLAWB_MAX_CONCURRENT_GIT_PUSHES` | Max concurrent `git-receive-pack` POST operations, in a pool separate from the read pool. The anon receive-pack `info/refs` advertisement runs in a third pool of the same size, disjoint from both, so an advertisement flood cannot shed a push either. Two per-source push caps are derived from this value (`/8`, floor 1) and have no env var of their own. Over-cap sheds a 503 + Retry-After. Default 32. |
| `GITLAWB_MAX_CONCURRENT_READS_PER_CALLER` | Max concurrent read ops a single caller may hold, so one caller cannot monopolize the read pool. Keyed on the resolved source IP, never the DID, and only as granular as `GITLAWB_TRUSTED_PROXY`: left unset, a node behind an edge or NAT keys every caller on the edge IP and this collapses to one global cap. Default 16. |
| `GITLAWB_MAX_CONCURRENT_PIN_TASKS` | Max post-push pin loops (IPFS + Pinata) running concurrently across all repos. This caps how many loops RUN at once, not how much object-id list memory the node retains: on the local IPFS path a loop parked waiting for a permit still holds its full list. Do not size memory from this knob alone. A loop over cap waits, never drops a pin. Default 8. |
Expand Down
18 changes: 13 additions & 5 deletions crates/gitlawb-node/src/api/ipfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8338,6 +8338,7 @@ mod tests {
#[tokio::test]
async fn get_by_cid_per_source_cap_sheds_same_source_admits_other() {
let mut state = crate::test_support::test_state_lazy();
state.db.pool().close().await;
// Global pool has room; the per-source cap is 1.
state.git_ipfs_walk_semaphore = Arc::new(Semaphore::new(8));
state.git_ipfs_walk_per_caller = crate::rate_limit::PerCallerConcurrency::new(1, 100);
Expand All @@ -8364,16 +8365,23 @@ mod tests {
"a source at its per-source /ipfs walk cap must shed 503 with global capacity free"
);

let bytes = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(body["error"], "overloaded");

// A DIFFERENT source is NOT shed by the per-source cap: it clears admission and
// proceeds (then errors on the lazy DB, which is not a 503).
// proceeds to the closed DB, which has a distinct db_unavailable error code.
let resp = ipfs_router(state)
.oneshot(get_cid(&cid, Some(other)))
.await
.unwrap();
assert_ne!(
resp.status(),
StatusCode::SERVICE_UNAVAILABLE,
"a different source must not be shed by the per-source cap"
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
let bytes = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(
body["error"],
crate::error::DB_UNAVAILABLE_CODE,
"a different source must clear admission and reach the closed database"
);
}

Expand Down
Loading
Loading