Skip to content

fix(submodule): bind writes to the validated checkout - #2335

Draft
Byron wants to merge 1 commit into
mainfrom
validate-submodule-path
Draft

Byron wants to merge 1 commit into
mainfrom
validate-submodule-path

Conversation

@Byron

@Byron Byron commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Tasks

This section is for Byron only. Models continuing this PR must not add, remove, check, uncheck, rename, or reorder checkboxes here.

  • refackiew

Everything below this line was generated by Codex (exact runtime model unavailable).

Created by Codex on behalf of Byron. Byron will review before this is ready to merge.

Submodule.module() validated the configured checkout path, then opened a Repo whose core.worktree could select a different directory through a symlink. Updating could overwrite files there; unforced remove(configuration=False) could delete that directory. This bug was discovered during the requested audit of GitPython's submodule writes against Git's existing symlink protection.

Bind the shared module handle to the validated checkout with GIT_WORK_TREE. Updates, removal, recursive traversal, and callers of module() then use that checked path. Opening the handle remains read-only. Git's own update repairs persisted core.worktree; this fix confines the GitPython handle without rewriting configuration during a query.

The audit traced add, clone, reconnect, update, move, remove, deinit, and root/recursive update paths. Their existing checkout checks or Git delegation remain in place. Git-supported metadata symlinks remain supported. A separate native configured-worktree canonicalization difference is recorded under the still-open GIX-14 ledger entry.

Validation

  • Added eight cases comparing updates and removal with Git, covering leaf and intermediate symlinks. Git preserves the external targets; GitPython failed before the fix and passes afterward on both backends.
  • Ran all of test/test_submodule.py and test/test_submodule_no_fetch.py: CLI had 484 passes, Gix had 485 passes, with their existing skips/xfail. Each initially had one failure because the sandbox could not access the user's GPG agent for an existing test's commit. That case passes on both backends with commit.gpgSign=false injected only into the test process; no persistent signing configuration changed.
  • Full mypy, basedpyright --warnings, Ruff check/format, ShellCheck with sourced fixtures, and git diff --check pass.
  • Post-commit codex review --commit 9d74656 found no actionable regression.
  • Git 2.54.0 is the executable baseline. Reference checkout 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd supplies submodule.c:validate_submodule_path() and builtin/submodule--helper.c:ensure_core_worktree().

Path validation remains a point-in-time check, as in Git; this change does not provide atomic protection against concurrent filesystem replacement.

Reported issue (user-authored, verbatim)

When doing anything that writes submodule repositories or worktrees, this always requires validate_submodule_path (see /Users/byron/dev/github.com/git/git) or equivalent to avoid writing through symlinks. Audit the code to make sure this happens.

Follow-up constraints (user-authored, verbatim)

But do not go against what Git does

Unless it's clearly a bug. This might mostly be an audit.

The requested audit against Git's submodule path validation found that
`Submodule.module()` checked the checkout path, then let `Repo` select a
different working tree from `core.worktree`. A symlink in that configured
path could redirect `update()` writes outside the checkout. Unforced
`remove(configuration=False)` could delete the external working tree.

Pass the checked checkout as `GIT_WORK_TREE` when opening the shared module
handle. This keeps updates, removal and recursive traversal on the validated
path without making repository queries rewrite configuration. Existing Git
metadata symlinks remain supported.

Git reference `6de20f6092dcf9bdb1c8efe03db4b70c82b423dd` validates checkout
components in `submodule.c:validate_submodule_path()` and repairs configured
worktrees before update in `builtin/submodule--helper.c:ensure_core_worktree()`.
Git 2.54.0 preserves external targets in the new update/deinit comparisons.
GitPython previously overwrote or deleted them. The regressions cover leaf
and intermediate symlinks, and pass on both CLI and Gix backends after the fix.

Record the independently observed GixPython 0.3.0 configured-worktree
canonicalization difference under the still-open `GIX-14` ledger entry.
The scoped submodule guard does not claim to fix native discovery upstream.

Validation: eight focused regression cases per backend; full `mypy` and
`basedpyright --warnings`; Ruff check/format; ShellCheck with sourced fixtures;
`git diff --check`.

Agent-Model: unknown
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants