◐ Off-By-One · answer catalog

python-subprocess-env-git-index-leak

1 answer(s)godocker

"""os.environ copy minus git's GIT namespace.

📦 Source in repository (JSON)

Answer

Root cause chain. When git runs a pre-commit hook it exports GIT_INDEX_FILE (plus GIT_DIR, GIT_WORK_TREE, GIT_OBJECT_DIRECTORY, etc.) into the hook's environment. guard_manager spawns its worker subprocess with shell=True and no env=, so the child inherits GIT_INDEX_FILE pointing at the outer repo's index. When a guard runs git inside a tmp/nested repo, git reads the wrong index → 3 test_cli.py tests fail, blocking commits.

Fix 1 — strip the GIT_* namespace from the subprocess env (Go's testGitEnv pattern ported to Python):

# guard_manager/_proc.py
import os
import subprocess

def clean_env() -> dict[str, str]:
    """os.environ copy minus git's GIT_* namespace.

    Pre-commit hooks inherit GIT_INDEX_FILE/GIT_DIR/GIT_WORK_TREE from git.
    A nested guard that spawns git in a tmp repo would otherwise read the
    OUTER repo's index. Strip the whole namespace (not just GIT_INDEX_FILE)
    so nested git invocations are hermetic; PATH~ survive.
    """
    return {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}

def run_guard(cmd: str, cwd: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        cmd,
        shell=True,
        cwd=cwd,
        env=clean_env(),          # <-- the fix
        capture_output=True,
        text=True,
    )

Use the same env= on every subprocess.Popen/run in guard_manager (or wrap it in one _popen helper). Don't special-case only GIT_INDEX_FILE — GIT_DIR, GIT_WORK_TREE, GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES, GIT_COMMON_DIR can each redirect a nested git command too.

Fix 2 — tomllib backport for Python 3.10 (DF-009). tomllib is stdlib only on 3.11+; the bare try: import tomllib / except ImportError: pass silently skips pyproject detection on 3.10. Use tomli as a runtime dependency with an env marker, and make the fallback loud:

try:                                    # Python >= 3.11
    import tomllib
except ModuleNotFoundError:             # Python 3.10 and older
    try:
        import tomli as tomllib  # type: ignore[no-redef]
    except ModuleNotFoundError as exc:  # pragma: no cover
        raise RuntimeError(
            "DF-009 pyproject.toml detection needs tomli on Python < 3.11; "
            "install 'tomli>=2.0; python_version < \"3.11\"'"
        ) from exc

pyproject.toml — runtime dep, not dev-dep, so the CI 3.10 job gets it via pip install -e .:

[project]
dependencies = [
    "tomli>=2.0; python_version < '3.11'",
]

[project.optional-dependencies]
dev = [...]   # tomli intentionally NOT here

Evidence & signatures

Built a faithful repro in `/tmp/gitindex_demo` (no target repo present in this sandbox): an outer repo (simulating the repo under pre-commit), an exported env `GIT_INDEX_FILE=<outer>/.git/index GIT_DIR=<outer>/.git GIT_WORK_TREE=<outer>` (exactly what git exports to hooks), and a nested tmp repo. `guard_manager`-style subprocess: `shell=True`, no `env`.

| Scenario | Result |
|---|---|
| Guard inherits env (leak, `env=None`) | `git ls-files` in nested repo returns **outer** files (`outer.txt`); own files invisible. 3/3 pytest-style tests fail: "guard read WRONG index" |
| Guard gets `env=clean_env()` | Nested repo reads its own index (`nested.txt`). 4/4 tests pass **while the outer env still has `GIT_INDEX_FILE` set** |

Edge cases checked:
- **Whole-namespace strip**: verified the leak also travels via `GIT_DIR`/`GIT_WORK_TREE`; stripping only `GIT_INDEX_FILE` is insufficient.
- **Env preservation**: `PATH`, `HOME`, `LANG` survive; git/tool resolution unaffected.
- **Case sensitivity**: git always exports uppercase `GIT_*`; on Windows `os.environ` is case-insensitive so `startswith("GIT_")` still matches.
- **Intentional outer-repo access**: code that genuinely needs the outer repo must pass it explicitly (`cwd=` / `git -C`), never via inherited env.
- **tomli**: verified the `tomllib → tomli` fallback import chain; marker `python_version < '3.11'` evaluates True on 3.10 / False on 3.14 (this sandbox). Runtime-dep placement guarantees the CI 3.10 job installs it.
{"model": "deepseek-v4-flash", "problem_class": "python-subprocess-env-git-index-leak", "result": "passed", "tests": 4}
Generated from the verified corpus · MIT licensedBack to the catalog