Skip to content

Git history proof: reject raw commits missing or duplicating the author header #86

Open

Description

@andy-zen-dev

Verified implementation finding (2026-10-23)

PR #81 head: a4761daa9fa87026ea106a3d722e7299b3de3b8b (target release/0.6.0).
Source: src/devostasis/codeanalysis/history_proof.py::commit_headers, blob f5f0445cba726571b88ac77fec6f440e58e22c78.

The parser checks exactly one tree and one committer header, but it does not check author header count. The same structural parser is used for independently checking raw historical commit objects. A Git-object digest proves byte integrity, not that the bytes form a well-shaped Git commit.

Reproduction and evidence

In a disposable Git repository, create a raw commit with a valid tree and committer, but without any author line. Repeat with two author lines. Write each raw commit with git hash-object --literally -t commit -w --stdin, and inspect with git fsck --strict.

On Git 2.47.3, independently repeated with both SHA-1 and SHA-256:

  • valid single-author commit: fsck exit 0, isolated exact-source commit_headers() accepts it;
  • missing author: fsck exit 1 (missingAuthor), isolated exact-source parser accepts it;
  • duplicate author: fsck exit 1 (multipleAuthors), isolated exact-source parser accepts it.

This is a confirmed parser-level inconsistency in the exact code at PR #81 head. End-to-end code verify/historical bundle acceptance is not yet tested and must not be assumed. This is distinct from #82 (Git tree ordering/duplicates) and #83 (persisted Integrity carrier arrays). Searching open and closed repository issues for missingAuthor, multipleAuthors, and commit-author header shape found no dedicated duplicate.

Proposed bounded implementation

In the commit header admission path, require exactly one syntactically valid author header while retaining existing tree/parent/committer checks. This is format validation only: it does not authenticate the person's identity, and it must not alter Git hashes, report semantics, history windows or model-free runtime.

Acceptance criteria

  1. Add regression cases for missing, duplicated, and malformed author headers for SHA-1 and SHA-256, validating the real raw-object admission/verification path rather than only a copied function.
  2. Positive controls must preserve ordinary root commits, merge commits and multiline gpgsig headers. Avoid rejecting supported legitimate Git formats.
  3. Confirm full offline pytest, v1/v2 historical verification, sample bundles and exact-head CI before any qualifying merge. Unknown/not-run checks are not PASS.
  4. Record any new structured admission behavior in the existing Code Analysis specification/conformance map, without reassigning permanent case identifiers.

Related PR: #81; related structural hardening: #82. Issue #83 remains independent.

Evidence limits: native Git and isolated parser reproduction passed; full Devostasis pytest/replay was NOT_RUN.

Activity

  1. andy-zen-dev commented on Oct 23, 2026

    @andy-zen-dev
    Author

    Implementation handoff for #86, scoped to PR #81 exact head a4761daa9fa87026ea106a3d722e7299b3de3b8b (src/devostasis/codeanalysis/history_proof.py blob f5f0445cba726571b88ac77fec6f440e58e22c78).

    The existing commit_headers() validates one tree and one committer but does not count or validate author. A minimal candidate fix:

    authors = [line[7:] for line in lines if line.startswith(b"author ")]
    # Extend the existing shape check with len(authors) == 1.
    author_parts = authors[0].rsplit(b" ", 2)
    require(len(author_parts) == 3, "commit author syntax")
    author_identity, author_epoch, author_zone = author_parts
    author_name, delimiter, author_email = author_identity.rpartition(b" <")
    require(bool(author_name.strip()) and delimiter == b" <" and author_email.endswith(b">")
            and author_epoch.isdigit() and (author_epoch == b"0" or not author_epoch.startswith(b"0"))
            and len(author_zone) == 5 and author_zone[:1] in (b"+", b"-")
            and author_zone[1:].isdigit(), "commit author syntax")

    Preserve the existing committer, tree, parent and timestamp rules. This is raw format admission, not identity authentication. Do not change historical bundles or semantic policies.

    Independent bounded checks run today: original extracted exact-source parser 6/24 expected outcomes (18 invalid author variants falsely accepted); patched parser 24/24; native Git 2.47.3 git fsck --strict in disposable SHA-1/SHA-256 repos 22/22 across root, multiline gpgsig, missing/duplicate author, malformed name, date and timezone. The parser-only merge control is not a native fsck claim. A prospective parametrized 20-case pytest addition exercises actual history_proof.graph() with raw commit bytes and exact framed hashes; it has been syntax-checked, NOT_RUN in Devostasis. The patch was syntactically validated and applied only to an isolated exact-context stub, not to the complete repo.

    Acceptance still requires applying it to the release-branch head, running the real tests/test_code_history_proof.py, complete pytest, v1/v2 and sample historical verification, docs/spec alignment, exact-head CI, then an independent later-wake review. No code has been committed; connected identity andy-zen-dev has pull=true, push=false. This comment does not claim full Git fsck equivalence or release readiness.


    2026-10-23 correction to the proposed patch (same PR #81 exact HEAD)

    The preceding candidate code snippet is not yet safe to adopt unchanged. A fresh bounded native Git 2.47.3 git fsck --strict oracle, with one disposable repository per case and both SHA-1/SHA-256 formats, found discrepancies that were absent from the original 22-case set. The first candidate accepts hash-valid but invalid author identities containing embedded NUL, extra </> characters in the name/email, and out-of-range positive author epochs. It also rejects a whitespace-only but nonempty author name which native Git accepts; Git also accepts a nonempty author name with empty email (Name <>).

    40/40 native-oracle cases matched by the refined standalone parser; original exact-head extracted function matched 10/40; earlier candidate matched 24/40. These numbers are for isolated parser checks against real Git objects, not full Devostasis tests or proof of complete Git-format parity.

    Refine the candidate after author_name, delimiter, author_email = author_identity.rpartition(b" <"):

    require(bool(author_name) and delimiter == b" <" and author_email.endswith(b">")
            and b"\x00" not in author_identity
            and b"<" not in author_name and b">" not in author_name
            and b"<" not in author_email[:-1] and b">" not in author_email[:-1]
            and author_epoch.isdigit()
            and (author_epoch == b"0" or not author_epoch.startswith(b"0"))
            and len(author_epoch) <= 19 and int(author_epoch) <= 9223372036854775807
            and len(author_zone) == 5 and author_zone[:1] in (b"+", b"-")
            and author_zone[1:].isdigit(), "commit author syntax")

    Keep len(authors) == 1 in the original shape check. Positive controls: regular and Unicode names, empty email, whitespace-only-but-nonempty name, maximal signed-64-bit epoch, multiline gpgsig. Negatives: missing/duplicate author, stray angle brackets, NUL, overflow, negative/zero-padded epoch and malformed timezone. A 32-variant prospective graph() pytest snippet is syntax-checked but NOT_RUN on an actual checkout.

    Full pytest, historical v1/v2 bundle replay, exact-head CI, real-code integration and later independent approval all remain NOT_RUN / NOT_PASS. No commit/branch/PR created: andy-zen-dev still has pull=true,push=false. The #81/#75 release gates are unchanged.

  2. andy-zen-dev commented on Oct 23, 2026

    @andy-zen-dev
    Author

    2026-10-23 follow-up: Git commit header ordering and non-semantic trailing parent

    Exact reviewed source: PR #81 head a4761daa9fa87026ea106a3d722e7299b3de3b8b, src/devostasis/codeanalysis/history_proof.py::commit_headers(), blob f5f0445cba726571b88ac77fec6f440e58e22c78. I am correcting the earlier author-only patch proposal in this same Issue; there is no new PR/commit.

    Confirmed new correctness gap: the current source and the previously proposed author-refinement candidate scan all parent lines anywhere in a hash-valid raw commit. Git only treats the contiguous parent prefix after tree and before author as ancestry. With a syntactically valid parent <real-oid> after committer, Git 2.47.3 git fsck --strict accepts the commit, but git rev-list --parents reports zero parents. The earlier candidate incorrectly derives one parent. This can poison claimed historical roots/ancestry even when object hashes are correct; it is not merely cosmetic ordering. Separately, the earlier candidate accepts invalid interleaved author/committer headers and a NUL in an unrelated header.

    Fresh isolated verification: 38 header-format variants × SHA-1/SHA-256 = 76/76 Git-admission verdicts matched by the combined candidate (prior refined candidate 50/76, 26 mismatches). An independent 8-placement × two-format 16/16 test also compared Git rev-list --parents parent counts with the candidate for all admitted commits. Proposed graph() regression cases: 22/22 PASS only in an extracted-function harness with local dependency stubs, NOT a real Devostasis checkout. Combined patch applies to the exact isolated original commit_headers() text and compiles. Native rejection via git update-ref is distinguished from fsck in the evidence.

    Bounded combined patch candidate (extends the earlier author fix, without new contract semantics):

    --- a/src/devostasis/codeanalysis/history_proof.py
    +++ b/src/devostasis/codeanalysis/history_proof.py
    @@ -2,13 +2,33 @@
         header, separator, _ = data.partition(b"\n\n")
         require(separator, "commit header terminator missing")
         lines = header.split(b"\n")
    +    # Raw object hashes establish integrity, not canonical commit-header order.
    +    require(b"\x00" not in header, "commit header contains NUL")
    +    require(lines and lines[0].startswith(b"tree "), "commit tree header order")
    +    first = 1
    +    while first < len(lines) and lines[first].startswith(b"parent "):
    +        first += 1
    +    require(first < len(lines) and lines[first].startswith(b"author "), "commit author header order")
    +    require(first + 1 < len(lines) and lines[first + 1].startswith(b"committer "), "commit committer header order")
         trees = [line[5:].decode("ascii") for line in lines if line.startswith(b"tree ")]
    -    parents = [line[7:].decode("ascii") for line in lines if line.startswith(b"parent ")]
    +    parents = [line[7:].decode("ascii") for line in lines[1:first]]
    +    authors = [line[7:] for line in lines if line.startswith(b"author ")]
         committers = [line[10:] for line in lines if line.startswith(b"committer ")]
    -    require(len(trees) == 1 and len(committers) == 1 and len(parents) <= 10000
    -            and len(parents) == len(set(parents)), "commit tree/parent/committer shape")
    +    require(len(trees) == 1 and len(authors) == 1 and len(committers) == 1 and len(parents) <= 10000
    +            and len(parents) == len(set(parents)), "commit tree/parent/author/committer shape")
         for ref in trees + parents:
             object_ref(ref, width)
    +    author_parts = authors[0].rsplit(b" ", 2)
    +    require(len(author_parts) == 3, "commit author syntax")
    +    author_identity, author_epoch, author_zone = author_parts
    +    author_name, email_delimiter, author_email = author_identity.rpartition(b" <")
    +    require(bool(author_name) and email_delimiter == b" <" and author_email.endswith(b">")
    +            and b"\x00" not in author_identity and b"<" not in author_name and b">" not in author_name
    +            and b"<" not in author_email[:-1] and b">" not in author_email[:-1]
    +            and author_epoch.isdigit() and (author_epoch == b"0" or not author_epoch.startswith(b"0"))
    +            and len(author_epoch) <= 19 and int(author_epoch) <= 9223372036854775807
    +            and len(author_zone) == 5 and author_zone[:1] in (b"+", b"-")
    +            and author_zone[1:].isdigit(), "commit author syntax")
         identity, epoch, zone = committers[0].rsplit(b" ", 2)
         require(identity.endswith(b">") and len(zone) == 5 and zone[:1] in (b"+", b"-")
                 and zone[1:].isdigit(), "commit committer syntax")

    Required acceptance: add real SHA-1/SHA-256 graph() regressions for reordered/interleaved headers, header NUL, normal root/parent/merge, and multiline gpgsig; specifically assert post-committer parent is an extension, not ancestry, while prefix parent remains an ancestry edge. Full pytest, conformance and historical v1/v2 replay, exact-head CI and fresh independent review remain NOT_RUN / NOT_PASS. No approval or merge is warranted yet. andy-zen-dev still has pull=true,push=false on upstream.

  3. andy-zen-dev commented on Oct 23, 2026

    @andy-zen-dev
    Author

    2026-10-23 additional exact-head QA: the proposed #86 fix still needs symmetric committer admission

    Source: PR #81 a4761daa9fa87026ea106a3d722e7299b3de3b8b, src/devostasis/codeanalysis/history_proof.py::commit_headers (blob f5f0445cba726571b88ac77fec6f440e58e22c78). This refines—not supersedes the ancestry-order finding in—the preceding comment.

    The combined candidate hardens author but still parses committer using only identity.endswith(b">") and epoch.lstrip(b"-").isdigit(). A hash-valid commit with committer Name> <a@b> 1700000000 +0000, committer Name <a@b> 01 +0000, or a negative committer epoch is accepted by that candidate, while Git 2.47.3 fsck --strict rejects it (verified on both SHA-1 and SHA-256). Conversely, Git accepts two spaces between the closing email bracket and epoch (Name <a@b> 1700000000 +0000), but the candidate rejects this valid form for author and committer.

    New independently executed oracle: 100 targeted variations × two object formats, each in a fresh repo using hash-object --literally, update-ref, and fsck --strict: prior candidate 160/200 matches; refined symmetric parser 192/200. The earlier 38-variation × two-format matrix still matches 76/76 after refinement. The remaining eight mismatches (across both formats) are intentional conservative rejections, not false acceptance: the signed-64-bit maximum committer timestamp cannot be represented by Python datetime, and Git accepts trailing author, committer, or tree extension headers that this strict proof parser rejects as duplicates. Do not claim complete equivalence with Git.

    Implementation refinement: use one helper for both identities and dates. Split on the final two ASCII spaces, then strip only extra ASCII spaces from the identity's right edge before checking name <email>; reject extra angle brackets; require unsigned, non-zero-padded decimal epoch bounded to signed 64-bit and a signed four-digit zone. Feed the validated committer epoch into the existing UTC timestamp conversion. Keep the previously verified tree, parent*, author, committer prefix and derive ancestry only from its contiguous parent prefix. This is structural admission only, not identity authentication or a Vital semantic-policy change.

    Required integration regression: include malformed committer name/email, negative/zero-padded committer epoch, valid double-space identity/date in both roles, normal roots/merges, trailing non-ancestry parent, and multiline gpgsig, for SHA-1 and SHA-256; run through real history-proof admission rather than only extracted functions.

    Verification boundary: candidate code compiles; the generated unified diff dry-runs against the isolated exact function text. Full Devostasis pytest, historical replay, real-module integration, upstream CI, and later independent acceptance remain NOT_RUN / NOT_PASS. This is a ready implementation handoff, not a commit or merged fix. Authenticated andy-zen-dev remains pull=true,push=false.


    2026-10-24 consolidated executable candidate / Git-oracle qualification

    At PR #81 exact HEAD a4761daa9fa87026ea106a3d722e7299b3de3b8b, I consolidated the earlier #86 author, committer, header-order and ancestry corrections into one proposed commit_headers() patch plus one shared _checked_commit_person() validator. Source target: src/devostasis/codeanalysis/history_proof.py, original blob f5f0445cba726571b88ac77fec6f440e58e22c78.

    Concrete behavior: require tree, parent*, author, committer as the commit prefix; count one author/committer; derive ancestry only from the contiguous prefix parents; reject NUL in the header; format-check both identities and their unsigned canonical epochs/timezones; preserve legitimately empty email/whitespace-only name/double spaces before epoch and multiline gpgsig. Hash integrity alone is insufficient. The candidate deliberately rejects ambiguous trailing duplicate author/committer extension fields, although Git fsck accepts them; this is a stricter, explicitly scoped offline-proof admission policy, not full Git fsck equivalence.

    New independent executable QA: Python 3.13.5, Git 2.47.3, 22 cases × SHA-1/SHA-256 in fresh disposable Git repositories. Native Git and the candidate agree on 40/44 admission verdicts; the remaining 4 are the stated conservative rejects. Therefore 44/44 cases meet the explicitly qualified candidate policy, including Git rev-list --parents checks for ordinary prefix ancestry versus a trailing extension parent. The original extracted commit_headers parser disagreed with native Git on 24/44 cases. A unified one-function patch passed git apply --check, application, and Python compilation only in a synthetic exact-function context. The source is still unchanged in GitHub.

    Prepared handoff checksums: unified patch SHA-256 d2e25d4579f3e5989512664decaa30d0e75d2be5f19a44fbdb9a4ff66591c669; runnable native-Git oracle SHA-256 f737b44f1a984419b413042eac8f1a06ed5b32c1cedbfa4fe5fcbb6ef256302f. Files are available as an attachment in this run's delivery, not as GitHub source files.

    Required before closure: apply to a real feature checkout targeting release/0.6.0 after confirming head/blob, add actual graph() and code verify regressions on both hash formats, run complete pytest plus historical/sample replay and installed-package gates, obtain exact-head CI and later independent review. Those repository-level checks are NOT_RUN, upstream workflows are action_required (NOT_PASS). GitHub identity andy-zen-dev is pull=true,push=false; 0 commits/PRs/merges from this handoff.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions