Skip to content

Code proof admission: reject duplicate and noncanonical entries inside each Git tree #82

Open

Description

@andy-zen-dev

Confirmed implementation gap

Revalidated at PR #81 head a4761daa9fa87026ea106a3d722e7299b3de3b8b on 2026-10-22. This records a previously prepared local review item in the repository; it is not a new semantic rule.

src/devostasis/codeanalysis/bundle.py::admit and src/devostasis/codeanalysis/history_proof.py::tree validate the object digest, mode, name and duplicate final file paths, but do not reject repeated entry names within one tree or noncanonical entry order. Two directory entries with the same name and disjoint children evade the final-file-path duplicate check. A correctly hashed but unsorted tree also passes these loops. Hash consistency alone does not establish an unambiguous Git path inventory.

Reproduction and impact

Use a disposable local repository with raw tree objects, without running repository source. Construct either (a) two dir tree entries pointing to different subtrees, or (b) entries z.py, a.py in that order. git hash-object -t tree -w --stdin can store these objects. Recursive listing can show paths that normal Git path lookup does not resolve consistently; git fsck --strict reports duplicateEntries or treeNotSorted. The current walker loops lack the corresponding structural rejection.

The prepared local regression artifacts cover SHA-1 and SHA-256, current trees and historical ancestors. Prior extracted-function checks are limited evidence, not full-product acceptance. Full checkout tests and historical replay have not been run in this review environment.

Scope

Add per-tree name uniqueness and Git canonical ordering checks to both admission paths. Keep existing byte/object/expansion limits, valid modes, supported formats, offline replay, and explicit unavailable evidence. Do not change Vital rules, thresholds, permanent conformance identifiers, source-selection policy, or execute observed source.

Acceptance

  • Duplicate directory names, file/directory name collisions and noncanonical order are rejected independently in current and historical trees.
  • Parameterized regressions cover SHA-1/SHA-256 and both paths; include ordinary files, directories, links and submodules as valid controls.
  • Ordering matches Git's byte ordering with the directory terminator, not locale or Unicode sorting.
  • Existing v1/v2 examples retain canonical bytes and verify; full pytest, code examples and history proof replay pass at the exact fixing head.
  • Independent review covers the final head before merge into release/0.6.0; Release 0.6.0: offline repository analysis and executable assurance #75 is not released ahead of this gate.

Related: #72, #73, #81. Those issues track the broad producer/proof capability; this issue isolates the concrete tree-admission defect. Existing open and closed issue search and #81 discussion were checked before filing.

Activity

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

    @andy-zen-dev
    Author

    Concrete repair for the current source/history walkers

    Apply to the existing #81 development path based on a4761daa9fa87026ea106a3d722e7299b3de3b8b, then propagate through release/0.6.0. This is a prepared patch, not a code commit, release approval or issue resolution.

    --- a/src/devostasis/codeanalysis/bundle.py
    +++ b/src/devostasis/codeanalysis/bundle.py
    @@ -68,16 +68,23 @@
         while stack:
             oid, prefix, parents = stack.pop()
             require(oid in tree_bytes and oid not in parents, "tree proof missing or cyclic")
             visited.add(oid); data = tree_bytes[oid]; offset = 0
    +        names_seen = set()
    +        previous_key = None
             while offset < len(data):
                 space = data.find(b" ", offset); end = data.find(b"\0", space)
                 require(space > offset and end > space + 1 and end + 1 + width // 2 <= len(data), "tree entry malformed")
                 mode, name = data[offset:space], data[space + 1:end]
                 require(b"/" not in name and name not in (b".", b".."), "tree name malformed")
    +            require(name not in names_seen, "duplicate tree entry")
    +            names_seen.add(name)
    +            key = name + (b"/" if mode == b"40000" else b"")
    +            require(previous_key is None or previous_key < key, "noncanonical Git tree ordering")
    +            previous_key = key
                 child = data[end + 1:end + 1 + width // 2].hex(); offset = end + 1 + width // 2
                 filename = prefix + name
                 if mode == b"40000":
                     stack.append((child, filename + b"/", parents | {oid}))
                 else:
                     require(mode in (b"100644", b"100755", b"120000", b"160000") and filename not in expected, "tree entry mode or duplicate path")
                     expected[filename] = (mode.decode("ascii"), "commit" if mode == b"160000" else "blob", child)
    --- a/src/devostasis/codeanalysis/history_proof.py
    +++ b/src/devostasis/codeanalysis/history_proof.py
    @@ -189,22 +189,29 @@
         while stack:
             oid, prefix, parents = stack.pop()
             require(oid not in parents, "historic tree cycle")
             data = objects.get("tree", oid)
             if data is None:
                 complete = False; continue
             offset = 0
    +        names_seen = set()
    +        previous_key = None
             while offset < len(data):
                 work[0] += 1
                 if work[0] > objects.limits["max_diff_work"]:
                     return entries, False
                 space = data.find(b" ", offset); end = data.find(b"\0", space)
                 require(space > offset and end > space + 1 and end + 1 + objects.width // 2 <= len(data), "historic tree entry malformed")
                 mode, name = data[offset:space], data[space + 1:end]
                 require(b"/" not in name and name not in (b".", b".."), "historic tree name malformed")
    +            require(name not in names_seen, "duplicate tree entry")
    +            names_seen.add(name)
    +            key = name + (b"/" if mode == b"40000" else b"")
    +            require(previous_key is None or previous_key < key, "noncanonical Git tree ordering")
    +            previous_key = key
                 child = data[end + 1:end + 1 + objects.width // 2].hex(); offset = end + 1 + objects.width // 2
                 filename = prefix + name
                 if mode == b"40000":
                     stack.append((child, filename + b"/", parents | {oid}))
                 else:
                     require(mode in (b"100644", b"100755", b"120000", b"160000") and filename not in entries, "historic tree mode/duplicate")
                     entries[filename] = (mode, child)

    Per-tree state is reset for every expanded object. Existing cycle, byte/object/leaf limits and historical work-budget early return are retained. A capped historical walk is still incomplete rather than a successful full proof.

    Regression shape: parameterize SHA-1/SHA-256 × source/historical ancestor × duplicate directory/unsorted names (8 negative cases). Build the malformed raw tree only in a temporary test repository; create an ordinary child commit for historical-only cases so source-tree admission cannot mask the historical path. Use tests/test_code_analysis.py's git, commit and analyze helpers, whose current signatures were re-read. Require the specific validation error rather than any failure. Retain ordinary canonical-tree controls and verify v1/v2 examples byte-for-byte.

    The updated local candidate also adds the per-tree uniqueness/order rule to docs/spec/code-analysis.md and contains those eight project tests. Full project execution is NOT_RUN in this environment; limited extracted-walker probes do not close the release gate. Do not apply earlier duplicate-only and canonical-order patches together: this single guard supersedes both.

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

    @andy-zen-dev
    Author

    2026-10-23 addendum to #82: reserved Git name parity (HEAD a4761daa9fa87026ea106a3d722e7299b3de3b8b)

    The source and historical tree walkers at this exact HEAD validate names with only b"/" not in name and name not in (b".", b"..") in src/devostasis/codeanalysis/bundle.py::admit and src/devostasis/codeanalysis/history_proof.py::tree. Neither validates Git-reserved component spellings. The earlier proposed patch in this Issue's comment 6334248799 also only addresses duplicate names/order; and a later prepared local supplement checked only literal .git, which is too narrow.

    A new native Git 2.47.3 offline oracle used separate disposable SHA-1 and SHA-256 repositories, a valid blob, and raw deliberately hashed trees for each case. 24/24 expected name classifications matched git fsck --strict (native Git outcome only; NOT Devostasis integration):

    Git tree entry name Native Git outcome, both formats
    .git, .GIT, .GiT, .git , .git. rejected as hasDotgit
    GIT~1, .git:evil rejected as hasDotgit
    .gitignore, .gitmodules, code.py, git, .git~1 accepted

    Concrete extension to #82's acceptance: in both current-source and historical-ancestor tree admission, reject Git-reserved name spellings consistently with the declared portable Git tree safety policy, not just literal .git. Use a common deterministic byte-level component predicate or explicitly shared structural validator so the walkers cannot diverge. Keep valid .gitignore, .gitmodules and ordinary names accepted. Cover both object formats and positive/negative controls in real full-path code verify regressions; ensure historical bundle replay stays intact. Do not loosen source path safety or infer that an accepted hash proves Git name validity.

    Limit: this probe establishes the listed Git 2.47.3 behavior only. It does not exhaust HFS/NTFS reserved-name behavior, prove end-to-end acceptance in Devostasis, or certify a fixing commit. Full pytest and upstream CI remain NOT_RUN/NOT_PASS for these new cases. No semantic Vital/policy/version change is requested.

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

    @andy-zen-dev
    Author

    2026-10-23 — #82 additional structural admission finding: .gitmodules has mode-specific Git validity

    HEAD independently examined: PR #81 a4761daa9fa87026ea106a3d722e7299b3de3b8b; current/history raw-tree readers are src/devostasis/codeanalysis/bundle.py::admit and history_proof.py::tree. This extends the existing #82 comment about .git-name aliases; it is not a separate Issue or a contract change.

    New observation: Git 2.47.3 git fsck --strict accepts .gitmodules (and its tested case/NTFS aliases) as a regular blob at modes 100644 or 100755, but rejects the same name as a 40000 directory, 120000 symlink or 160000 gitlink. In particular, a positive .gitmodules blob is not a positive .gitmodules directory: an earlier local draft accidentally treated the directory as valid. Git reports gitmodulesBlob / gitmodulesSymlink violations for these malformed shapes.

    The native-Git matrix covered SHA-1 and SHA-256, names .gitmodules, .GITMODULES, .gitmodules , .gitmodules., GITMOD~1, .gitmodules:evil, with .gitmodulesX and .gitignore controls, across all five supported raw-tree modes: 80/80 observed cases. It extends the previously reported 24/24 Git-name cases. Results are native Git evidence only; they are not full Devostasis verification.

    Prepared implementation candidate: centralize per-entry validation in one shared pure helper used by both walkers. In addition to the already-required exact byte-order comparator (append / to directory names), within-tree uniqueness, valid modes and .git aliases, normalize the tested .gitmodules aliases using ASCII case-folding/trailing dot-or-space handling and require mode in (100644,100755). The helper's local checks matched 24/24 + 80/80 native-Git observations and 14/14 local structural controls. An uncommitted unified source patch and two-reader regression tests were prepared. git apply --check/apply passed only against an exact-context fragment mirror, not a real checkout; Python syntax compilation passed.

    Acceptance/verification still needed: test malformed and positive .gitmodules entries through both actual bundle.admit and historical tree paths (SHA-1/SHA-256), check against existing v1/v2 examples, run full offline pytest and historic replay at the fixing HEAD, then obtain a later independent review. Do not claim all HFS/NTFS fsck cases are implemented: the shared helper covers only a bounded verified family. No threshold, historical identity, Vital or model-free rule change is requested.

    Current restrictions: the observed GitHub identity has pull=true, push=false, so this comment is the durable handoff, not a pushed branch/PR, a passing CI gate, or a release approval.

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

    @andy-zen-dev
    Author

    2026-10-23 — correction to #82 Git-name test authority: .gitmodules gitlink results depend on the referenced object type

    PR #81 exact source head: a4761daa9fa87026ea106a3d722e7299b3de3b8b (unchanged on re-read). This corrects the over-broad statement in my earlier #82 comment 6361592793 that git fsck --strict universally rejects mode 160000 for .gitmodules aliases. That statement is not generally true.

    Using Git 2.47.3 and disposable repositories, I generated correctly hashed raw tree/commit objects containing an entry 160000 <alias>\0<OID>, then ran git fsck --strict --no-reflogs on the referenced root commit. The critical difference is what object that OID identifies:

    Tree entry Referenced object behind OID Native fsck --strict
    160000 .gitmodules existing blob exit 0 (not rejected)
    160000 .gitmodules existing commit exit 1, gitmodulesBlob: non-blob found at .gitmodules

    The difference holds for six aliases (.gitmodules, .GITMODULES, .gitmodules , .gitmodules., GITMOD~1, .gitmodules:evil) in both SHA-1 and SHA-256: 24/24 expected type-sensitive cases reproduced.

    Important interpretation: a Gitlink semantically denotes a commit, not a blob. Native fsck accepting a blob-backed mode-160000 entry does not make it a well-typed Gitlink. Its special .gitmodules check can behave differently depending on referenced object type. The prior 80/80 mode/name matrix and statement about universal rejection must therefore be read with this type-correct-target precondition, rather than as a proof that fsck validates name/mode pairs in isolation.

    After making that test precondition explicit (mode 160000 uses an actual commit OID, tree mode uses a tree OID, file/link modes use blob OIDs), the independent local proposed name/order validator matches 165/165 scoped, parameterized Git-name/mode/order assertions. This is local Git-oracle validation only, not a passing Devostasis checkout or complete Unicode/HFS/NTFS parity claim.

    Bounded implementation guidance: maintain fail-closed structural admission for #82; it is legitimate for the product to reject dubious .gitmodules gitlink entries even when fsck is permissive about blob-backed OIDs. Do not relax a conservative check merely to match this incidental native-fsck behavior. But make the test fixture's target-object type explicit, and distinguish Git agrees for validly typed fixtures from exact parity for every malformed cross-type object. Regressions still must exercise both real bundle.admit and proven historical tree paths, SHA-1/SHA-256, full pytest/example replay and a later exact-head independent review before merge.

    This is a correction of evidence qualification in the existing Issue, not another issue, a semantic Vital change, an upstream test PASS or proof of release readiness. No source write/PR/merge was performed.

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

    @andy-zen-dev
    Author

    2026-10-23 — #82 addendum: .gitattributes needs a distinct, mode-sensitive Git-tree admission rule

    Scope / exact HEAD: PR #81 a4761daa9fa87026ea106a3d722e7299b3de3b8b. I re-read src/devostasis/codeanalysis/bundle.py::admit (blob 3ac0d6c64c2a8ba7c6310e73bf6dfb9e2120d1f7), src/devostasis/codeanalysis/history_proof.py::tree (blob f5f0445cba726571b88ac77fec6f440e58e22c78), and the shared path predicate. Both raw-tree walkers check slash / dot / dotdot but not the special .gitattributes name+mode combination. The pending #82 duplicate/order repair also omits this case. No separate Issue is needed.

    Independent native-Git evidence: Git 2.47.3, 80 disposable repository cases: SHA-1 and SHA-256 × 8 tested tree-entry spellings × 5 supported modes (100644, 100755, 120000, 160000, 40000). Entries were byte-encoded raw trees written with git hash-object --literally -t tree -w --stdin, referenced from ordinary well-formed root commits, and examined with git fsck --strict --no-reflogs. Referenced OIDs were type-correct: blob for regular/symlink entries, commit for gitlink, tree for directory. Outcomes agreed between hash formats.

    Names tested Regular 100644/100755 Symlink 120000 Gitlink 160000 Tree 40000
    .gitattributes, .GITATTRIBUTES, .gitattributes , .gitattributes., GITATT~1, .gitattributes:evil accepted accepted rejected (gitattributesBlob) rejected (gitattributesBlob)
    .gitattributesX, .gitignore accepted accepted accepted accepted

    24/80 cases were Git-rejected (six special spellings × two invalid modes × two hash formats). This is important because .gitattributes is not identical to .gitmodules: the tested .gitattributes symlink is accepted by Git, whereas the prior #82 .gitmodules symlink cases are rejected. A shared validator should not blanket-reject all symlinks for both names.

    Implementation-facing acceptance addition: extend the shared structural tree-entry admission proposed in #82 to handle these special spellings/modes consistently in both current-tree and historical-tree paths. Add positive and negative SHA-1/SHA-256 regressions at the actual bundle.admit and history-proof interfaces; preserve positive .gitignore/.gitattributesX and ordinary symlink cases. Preserve canonical tree bytes and historical replay. Do not infer that hash verification implies Git-shape validity; do not modify Vital semantics.

    Evidence limit: this verifies native Git behavior and the missing explicit name/mode admission check by exact-head source inspection, not a completed devostasis code verify exploit, full pytest, bundle replay, fix commit, or release approval. All those remain NOT_RUN / NOT_PASS as applicable. Existing #82 is still open.

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