Skip to content

Execute structure gate suites from the index #51

Description

@ckrough

Follow-up to #41 (PR #48). Discovery was narrowed to tracked paths; the bytes executed at those paths were not.

Problem

scripts/gates/structure.sh Stages C and D select paths with git ls-files --cached, then hand each path to python3 -m unittest or to bash. Both open the working-tree file. Index membership of a path says nothing about the content executed.

Exploitation: overwrite the body of the tracked scriptorium/skills/docs/scripts/test_check_markdown.py with arbitrary Python and do not stage it. Stage C selects the path from the index and executes the modified content. Identically for Stage D: overwrite scripts/gates/test_structure.sh in place without staging and it runs. A tracked symlink whose target is untracked working-tree content has the same shape.

Why this is worth fixing

Same threat model as #41: the gate is the test binding in .daedalus/config.json, so it runs inside the Daedalus pipeline where an implementer subagent writes files into a worktree before any human reads the diff. In that model the writer has arbitrary worktree write, so #41's control reduces "introduce any new executed path" to "overwrite one of the existing tracked files", which is not a meaningful barrier for an automated writer.

Raised as a HIGH finding by the security review on PR #48 and dispositioned out of scope there, correctly: the fix is a different change from the one #41 specified. The merged Discovery rule: header paragraph in structure.sh already states the limitation honestly ("What this does NOT buy: pinning the CONTENT of a path that is already tracked"), so the gap is documented in the script but was not tracked in an issue until now.

Relationship to #49

#49 covers a different vector: untracked sibling modules imported via Python's sys.path[0]. Its acceptance criteria are satisfiable with python3 -P or -I alone, which does not close this issue, and closing this issue does not by itself close #49.

One fix closes both: execute from a clean checkout of the index (git archive or a throwaway git worktree) rather than from the working tree. That candidate is already listed in #49's Notes. Whichever lands first should state in its PR which of the two vectors it closes, so the other is not assumed covered.

Acceptance Criteria

  • A tracked test suite whose working-tree content differs from its indexed blob does not execute with the unstaged content: either the indexed content runs, or the gate refuses and reports the drift.
  • The same holds for Stage D's scripts/gates/test_*.sh.
  • A tracked symlink resolving to untracked working-tree content is not executed.
  • A gate self-test covers it, asserting that a module-level sentinel written by unstaged content is never created, following the case_untracked_python_refused pattern established in Stop the structure gate from executing untracked test files #41.
  • The Discovery rule: header paragraph in structure.sh is updated to describe the guarantee that now holds, replacing its current "What this does NOT buy" clause about unstaged content.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions