fix(778): backslash is a metacharacter too, or the none arm's proof does not hold
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 18s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
review-verdict/h10 Awaiting review verdict for cd1b286
Review verdict / Set review-verdict status (pull_request_target) Successful in 16s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m56s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m49s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 18s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
review-verdict/h10 Awaiting review verdict for cd1b286
Review verdict / Set review-verdict status (pull_request_target) Successful in 16s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m56s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m49s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Self-found while stress-testing the superset claim I introduced one commit earlier —
the crux the new classifier's safety rests on.
`none` authorises a DENY on the stated grounds that nothing can possibly govern this
base, so its premise must hold unconditionally, not usually. The superset was "literal
prefix + .* + literal suffix", which is sound for every glob dialect EXCEPT one case:
gobwas/glob reads `\{` as a LITERAL brace, so a rule `a\{b` governs the base `a{b`,
while a superset treating `\` as an ordinary character builds `a\.*b`, misses, and
denies a base that is in fact protected.
Verified before and after: with `\` outside the metacharacter class the classifier
answered `none` for that pair; with it inside, `undecidable` -> ask. 18 adversarial
rule/base pairs (brace alternation, negated and ranged classes, `**`, leading and
trailing metacharacters, unicode, empty alternation) all answer `undecidable`, never
`none`, so no dialect-matching case falls through the deny arm.
Git ref rules make this nearly unreachable — a branch name may not contain `*`, `?`,
`[` or `\` — but `{` IS legal in one, and "nearly unreachable" is not the standard for
the arm that issues a deny. Checked with `git check-ref-format` rather than assumed.
refs #778
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -437,7 +437,16 @@ else
|
||||
# case-insensitively (matching Gitea's EqualFold). Decidable; inspect the rule.
|
||||
# undecidable — some glob rule COULD govern this base. Tested with a provable SUPERSET of any
|
||||
# glob dialect: literal prefix before the first metacharacter, `.*`, literal
|
||||
# suffix after the last. If even that does not match, no dialect can.
|
||||
# suffix after the last. If even that does not match, no dialect can, because
|
||||
# every dialect requires the literal head and tail to match literally.
|
||||
#
|
||||
# BACKSLASH counts as a metacharacter for that purpose, and it is the one case that breaks the
|
||||
# superset proof if it does not. gobwas/glob reads `\{` as a LITERAL brace, so a rule `a\{b`
|
||||
# governs the base `a{b` — while a superset that treated `\` as literal would build `a\.*b`,
|
||||
# fail to match, and answer `none`, i.e. deny a base that IS protected. Git ref rules make this
|
||||
# nearly unreachable (a branch name may not contain `*`, `?`, `[` or `\`, though it MAY contain
|
||||
# `{`), but `none` is the arm that authorises a DENY on the stated grounds "nothing can govern
|
||||
# this base", so its premise has to hold unconditionally rather than usually.
|
||||
# none — nothing can possibly govern the base, so it is genuinely unprotected.
|
||||
#
|
||||
# `undecidable` asks rather than granting or denying. Over-matching would auto-grant on a base
|
||||
@@ -447,7 +456,7 @@ else
|
||||
# directions, and it is rare: the repo's rule today is the plain name `main`.
|
||||
bp_verdict=$(printf '%s' "$bp_list" | jq --arg b "$base_ref" -c '
|
||||
def esc: gsub("(?<c>[.+?^${}()|\\[\\]\\\\])"; "\\" + .c);
|
||||
def offs: [match("[*?\\[\\]{}]"; "g").offset];
|
||||
def offs: [match("[*?\\[\\]{}\\\\]"; "g").offset];
|
||||
def superset: . as $n | (offs) as $o
|
||||
| ($n[0:$o[0]] | esc) + ".*" + ($n[($o[-1]+1):] | esc);
|
||||
. as $rules | $b as $base |
|
||||
|
||||
@@ -65,6 +65,12 @@ if url.rstrip("/").endswith("/branch_protections"):
|
||||
# The LIST endpoint, consulted only after a 404 on the by-name lookup. Gitea keys the by-name
|
||||
# endpoint on the RULE name, so a base covered by a glob rule 404s there while being protected.
|
||||
mode = (state / "bp").read_text().strip()
|
||||
if mode == "ESCAPED-META-RULE":
|
||||
# gobwas/glob reads `\\{` as a LITERAL brace, so this rule governs the base `a{b`. If the
|
||||
# superset treats `\\` as literal it builds `a\\.*b`, misses, and answers `none` — denying a
|
||||
# base that IS protected.
|
||||
respond(json.dumps([{"branch_name": "a\\{b", "enable_status_check": True,
|
||||
"status_check_contexts": ["review-verdict/h10"]}]))
|
||||
if mode == "CASEFOLD-RULE":
|
||||
# Gitea compares a plain (non-glob) rule name with EqualFold, so a rule named `MAIN`
|
||||
# governs base `main`. A case-SENSITIVE comparison here would miss it and deny.
|
||||
@@ -107,7 +113,8 @@ if "/branch_protections/" in url:
|
||||
# curl itself fails: no body, and `-w` prints 000.
|
||||
respond("", "000")
|
||||
if mode in ("NOT-FOUND", "GLOB-RULE", "LIST-UNREADABLE", "REGEX-META-RULE",
|
||||
"GLOB-WITH-DOT-RULE", "CHARCLASS-RULE", "CASEFOLD-RULE"):
|
||||
"GLOB-WITH-DOT-RULE", "CHARCLASS-RULE", "CASEFOLD-RULE",
|
||||
"ESCAPED-META-RULE"):
|
||||
respond('{"message":"not found"}', "404")
|
||||
if mode == "FORBIDDEN":
|
||||
respond('{"message":"token does not have at least one of required scope(s)"}', "403")
|
||||
@@ -654,3 +661,26 @@ def test_a_plain_rule_name_is_matched_CASE_INSENSITIVELY(hook):
|
||||
assert "NO branch protection" not in reason, (
|
||||
"a rule named 'MAIN' governs base 'main' in Gitea but was missed by a case-sensitive compare")
|
||||
assert "deny" not in reason
|
||||
|
||||
|
||||
def test_a_BACKSLASH_ESCAPED_metacharacter_in_a_rule_name_is_undecidable_not_absent(hook):
|
||||
"""The one case that breaks the superset proof the `none` arm rests on.
|
||||
|
||||
`none` authorises a DENY on the stated grounds that nothing can possibly govern this base, so
|
||||
its premise must hold unconditionally. gobwas/glob reads `\\{` as a LITERAL brace, so the rule
|
||||
`a\\{b` governs the base `a{b`; a superset that treated `\\` as an ordinary character would build
|
||||
`a\\.*b`, fail to match, and deny a base that is in fact protected. Treating backslash as a
|
||||
metacharacter restores the property.
|
||||
|
||||
Git ref rules make this nearly unreachable — a branch name may not contain `*`, `?`, `[` or `\\`
|
||||
— but `{` IS legal in a branch name, and "nearly unreachable" is not the standard for the arm
|
||||
that issues a deny.
|
||||
"""
|
||||
hook.set_base("a{b")
|
||||
hook.drop_recorded_base()
|
||||
hook.set_branch_protection("ESCAPED-META-RULE")
|
||||
reason = hook.reason()
|
||||
assert "NO branch protection" not in reason, (
|
||||
"a rule whose escaped brace governs this base was reported as unable to govern it")
|
||||
assert "ask" in reason, (
|
||||
"an escaped-metacharacter rule is undecidable here and must ask")
|
||||
|
||||
Reference in New Issue
Block a user