Skip to content

fix(relay/git): accept git-legal + and @ in ref names - #4214

Open
alanshurafa wants to merge 1 commit into
block:mainfrom
alanshurafa:fix/refname-plus-at-4194
Open

fix(relay/git): accept git-legal + and @ in ref names#4214
alanshurafa wants to merge 1 commit into
block:mainfrom
alanshurafa:fix/refname-plus-at-4194

Conversation

@alanshurafa

@alanshurafa alanshurafa commented Aug 2, 2026

Copy link
Copy Markdown

Summary

is_safe_refname allows only [a-zA-Z0-9_./-], so git-legal ref names containing + or @ cannot be pushed at all — the push 400s at pre-CAS manifest validation. The reported real-world case is refs/heads/test/842+841-devnet in OriginTrail/dkg (1 ref out of 1,339 blocks the mirror); @ covers the equally common dependabot pattern refs/heads/dependabot/npm_and_yarn/@types/node-1.2.3.

This PR widens the shared predicate's alphabet to [a-zA-Z0-9_./+@-], and closes the two places that would otherwise re-reject or silently drop the newly accepted refs:

  • RefPattern::parse's literal-segment alphabet in buzz-core widens to match, so every ref the relay now accepts can also be named by a branch-protection rule (literal matching is segment-equality, not glob, so neither character carries metacharacter meaning).
  • is_emittable_ref in manifest_event.rs had a private re-spelling of the old alphabet and would have silently omitted +/@ refs from the kind:30618 ref-state event — a branch that pushes, hydrates, and clones fine but is invisible in every branch lister (desktop and web both read kind:30618). It now delegates character/structure rules to the shared is_safe_refname (keeping only the NIP-34 heads/tags namespace filter local), so this filter can never again drift from what the write path accepts. A test pins the round-trip.

Why this is safe (verified against every consumer of the predicate):

  • Refnames never become object-store key components — packs/manifests are digest-keyed and the pointer key is repos/<community>/<owner>/<repo>/pointer; refnames live inside the manifest JSON body. (The issue suggested percent-encoding at the storage boundary; it turns out none is needed.) No encoding, no collision surface.
  • On hydrate, refnames become loose-ref file paths; + and @ are valid filename bytes on Unix and NTFS, and the structural checks (.., //, control chars, prefix) are untouched.
  • The info/refs fast path re-runs the same predicate before emitting refnames into pkt-line bytes; both characters are inert there.
  • Refnames reach no subprocess argv and no shell; for-each-ref output parsing splits on whitespace, which git forbids in refnames.
  • The two @ forms git forbids stay unreachable: a refname that is entirely @ cannot occur behind the mandatory refs/ prefix, and @{ requires {, which remains outside the alphabet. (git check-ref-format confirms refs/heads/@ and refs/@/x are legal git, so no per-component @ restriction is added.)

Adjacent and deliberately untouched:

  • The predicate today accepts a few names git itself refuses (.lock suffix, leading-dot components). Not reachable in practice — refs enter manifests via receive-pack/for-each-ref, which enforce git's rules — and tightening is a separate concern from this widening.
  • The desktop app still has three client-side validators with the old alphabet (clean_branch in src-tauri/commands/project_git_exec.rs, a second clean_target_ref in project_git_diff.rs, and BRANCH_CHARACTERS in src/features/projects/lib/projectBranches.ts). Those are visible client-side rejections (a user can't select/create such a branch from the desktop UI), not silent server-side drops, and fixing them means touching the desktop TS test suite — happy to do that here if preferred, but it seemed better as a follow-up so the relay fix isn't blocked on desktop tooling. Two hazards for whoever picks it up: clean_branch's output is passed as a bare refspec positional to git fetch (a leading + means force there — prefix refs/heads/ or guard it), and its trim_start_matches("refs/heads/") strips repeatedly, so refs/heads/@ would reduce to @, git's HEAD alias. Server-side, this PR is complete: push → CAS → hydrate → advertisement → kind:30618 all accept the same set.
  • The push-policy endpoint's own ref gate is byte-range-based and wider than is_safe_refname, so git-legal refs outside ASCII (e.g. non-ASCII branch names) still authorize, run receive-pack, and then 400 at manifest validation — the same failure shape Git: is_safe_refname rejects legal git ref names containing '+' #4194 reports, for a much bigger character class. Unifying those two gates is a larger conversation than two characters; flagging rather than fixing here.

Related issue

Fixes #4194. Duplicate search at filing (2026-08-02 00:59 UTC): none found. Update: #4244 and #4257 have since been filed for the same issue. Both widen the shared predicate only; neither covers the kind:30618 emission filter (is_emittable_ref) or the RefPattern literal alphabet, so +/@ refs would push but stay invisible to branch listers and unnameable by literal protection rules. This PR covers all three surfaces.

Testing

  • cargo test -p buzz-core git_perms — 35 passed (new: literal patterns with +/@ parse and match).
  • cargo test -p buzz-relay --lib api::git — passed (new predicate cases in both manifest.rs and hydrate.rs test suites: the reported + ref, the dependabot @ ref, refs/tags/v1.0.0+build.5, refs/heads/user@host, refs/heads/@, refs/@/x accepted; @{, traversal, control chars, prefix violations still rejected).
  • cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --all -- --check — clean.
  • Toolchain note: built on Windows with x86_64-pc-windows-gnu (same setup as fix(audit): hash created_at at the precision Postgres stores #2638); Postgres-gated suites (#[ignore]) not run.

@alanshurafa
alanshurafa requested a review from a team as a code owner August 2, 2026 00:59
is_safe_refname allowed only [a-zA-Z0-9_./-], so git-legal refs like
refs/heads/test/842+841-devnet (a real branch in OriginTrail/dkg) or
dependabot's @-scoped npm branches could not be pushed at all: the push
400s at pre-CAS manifest validation after receive-pack already ran.

Widen the shared predicate's alphabet with '+' and '@'. Both characters
are inert everywhere the predicate guards: refnames live inside manifest
JSON (never object-store keys), hydrate writes them as loose-ref paths
(both are valid filename bytes on Unix and NTFS), the info/refs fast
path emits them into pkt-line payloads where they carry no meaning, and
refnames never reach a shell or subprocess argv. The @-forms git forbids
stay unreachable: a whole-refname '@' cannot occur behind the mandatory
refs/ prefix, and '@{' needs '{', still outside the alphabet.

Widen RefPattern::parse's literal-segment alphabet identically so every
ref the relay now accepts can also be named by a branch-protection rule
(matching is segment-literal equality; no glob semantics involved).

Fixes block#4194

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Agent57 <agent57@shurafa.com>
@alanshurafa

Copy link
Copy Markdown
Author

@block/buzz-oss-team — could a maintainer please approve the workflow runs for this fork PR and do a code-owner review? The reachable checks (Semgrep OSS, zizmor, DCO) pass and the branch is rebased on current main (28ae6cd). Thanks!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Git: is_safe_refname rejects legal git ref names containing '+'

2 participants