block / block/buzz

Git: pre-receive hook fails closed on pushes with more than ~20 refs; denial reasons never reach the pusher

Open
#4,193 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

# Git: pre-receive hook fails closed on pushes with more than ~20 refs, and the denial reason never reaches the pusher

## Summary

Pushing a real-world repository into Buzz's git hosting is effectively impossible without insider knowledge: any push carrying more than roughly 20 ref updates is atomically rejected with a bare `pre-receive hook declined` on every ref, and the human-readable denial reasons the policy endpoint computes are never surfaced to the client.

## Reproduction

Mirroring `OriginTrail/dkg` (1,339 heads+tags; 2,898 refs with GitHub's `refs/pull/*`) into a repo announced via kind 30617 with a valid `buzz-channel` binding, pushed by the **repo owner key** (so role = Owner, which `evaluate_push` should wave through):

| Push shape | Result |
|---|---|
| `git push --mirror` (2,898 refs) | every ref `! [remote rejected] … (pre-receive hook declined)` |
| 25-ref batches | rejected |
| ≤20-ref batches | **accepted** |
| single refs | accepted |

Same commit content, same pusher, same repo — only the ref count per push changes the outcome. The relay log shows only:

```
WARN git receive-pack report-status contains a rejected (ng) ref update
WARN receive-pack exited non-zero (e.g. pre-receive hook decline); skipping CAS publish and kind:30618
```

— no policy denial is logged and none is transmitted in the sideband, so the pusher cannot distinguish "role denied" from "hook infrastructure gave up".

## Likely cause

`crates/buzz-relay/src/api/git/policy.rs`:

- `MAX_CALLBACK_AGE_SECS = 30` — "Push is synchronous so 30s is generous". The hook computes `git merge-base --is-ancestor` **per ref** before POSTing the HMAC-signed callback; with thousands (or even dozens) of refs on a cold ephemeral hydration, the callback timestamp exceeds the TTL by the time it is assembled → fail-closed 403 → every ref denied (push is atomic per `evaluate_push`).
- Fail-closed is the right default, but the failure mode here is indistinguishable from a permissions denial.

## Suggested fixes (independent, any subset helps)

1. **Surface denial reasons.** `HookCallbackResponse` already carries per-ref reasons; print them to the git sideband (`pre-receive` stderr is shown to the pusher verbatim) so `requires admin role`, `callback expired`, etc. are visible.
2. **Start the TTL clock at callback assembly, or scale it with ref count** — the current constant assumes per-push cost is O(1) when it is O(refs).
3. **Batch or skip the per-ref ancestor checks** (`git merge-base --is-ancestor` per ref is the expensive step; for creates from the zero OID it is a constant `false` and needs no subprocess at all — which is every ref of an initial mirror push).

## Environment

Relay at `63496cc1d`, macOS host, pushes over HTTPS via Tailscale Serve with `git-credential-nostr`; observed 2026-08-01.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.