Skip to content

safe.bareRepository: default to "explicit" with WITH_BREAKING_CHANGES#2072

Open
dscho wants to merge 11 commits intogitgitgadget:masterfrom
dscho:make-safe.bareRepositories-the-default-in-git-3.0
Open

safe.bareRepository: default to "explicit" with WITH_BREAKING_CHANGES#2072
dscho wants to merge 11 commits intogitgitgadget:masterfrom
dscho:make-safe.bareRepositories-the-default-in-git-3.0

Conversation

@dscho
Copy link
Copy Markdown
Member

@dscho dscho commented Mar 24, 2026

In one of my projects, I work exclusively on bare repositories. During one of the tests, I noticed that safe.bareRepository is not yet enabled by default. This strikes me as undesirable, and I deem Git v3.0 the most logical opportunity to change the default.

Cc: Patrick Steinhardt ps@pks.im

@dscho dscho self-assigned this Mar 24, 2026
dscho added 11 commits March 27, 2026 18:30
It is all too easy for an attacker to embed a bare repository with
malicious hooks inside a cloned project. Even a seemingly harmless
shell prompt calling `git status` would then trigger those hooks via
fsmonitor. The `safe.bareRepository` setting, when set to `explicit`,
defends against this class of attack, and the test suite should be
ready for the day this becomes the default, most likely in Git v3.0.

The `test_hook` helper function (which gained `--git-dir` support in
a preceding commit) is used throughout the test suite to install hooks
into repositories. When the target is a bare repository, `test_hook
-C <bare>` fails under `safe.bareRepository=explicit` because the
`-C` flag triggers implicit bare repository discovery. Replace all
such calls with `test_hook --git-dir <bare>`.

Some diff hunks also contain adjacent `git -C` to `git --git-dir`
conversions for commands like `test_cmp_refs`, `git config`, or `git
update-ref` that happen to appear in the same hunk as the `test_hook`
change. These are the same kind of fix and would be difficult to
separate into individual hunks.

This commit was produced with the help of Claude Opus 4.6 acting as a
sophisticated keyboard. It was told to replace every `test_hook -C`
with `test_hook --git-dir` (and likewise for `git -C` and
`test_cmp_refs -C` in the same hunks). The dominant pattern can be
verified:

    git show HEAD |
    sed 's/test_hook --git-dir/test_hook -C/g' |
    sed -ne '/^diff/,/@@/d' -e 's/^[-+]//p' |
    uniq -u

Any residual output consists of the adjacent `git -C` to
`--git-dir` conversions mentioned above.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Even if the Git community ultimately decides not to change the default
of `safe.bareRepository`, it is good test hygiene to have a test suite
that would survive such a change unscathed. A preceding commit taught
`test-tool` a `--git-dir` option (mirroring what `git` itself does:
setting `GIT_DIR` in the environment). This commit puts it to use.

Replace `test-tool -C <bare>` invocations with `test-tool
--git-dir=<bare>` so that bare repositories are accessed explicitly
rather than through implicit discovery. In t1460-refs-migrate.sh, this
simplifies the `print_all_reflog_entries` helper considerably: the
previous version needed an if/else branch to dispatch between `-C` and
a `GIT_DIR=` prefix, whereas now both `test-tool` and `git` accept the
same `--git-dir` flag and the helper can pass `$repo_flag` through
uniformly.

Some diff hunks also contain adjacent `git -C` to `git --git-dir`
conversions that share the same hunk as the `test-tool` change.

This commit was produced with the help of Claude Opus 4.6 acting as a
sophisticated keyboard. It was told to replace `test-tool -C <bare>`
with `test-tool --git-dir=<bare>` and to simplify the surrounding
control flow where the new flag made branching unnecessary.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
If the `safe.bareRepository` default were to change to `explicit` one
day (as is being discussed for Git v3.0), the test suite must not
silently break in places that configure bare repositories via
`test_config -C <bare>`. The `-C` flag triggers implicit discovery
which would then be rejected. A preceding commit taught `test_config`
and `test_unconfig` a `--git-dir` option so that bare repository
configuration can be set and cleaned up with the same familiar
one-liner used everywhere else. This commit converts all call sites.

Replace `test_config -C <bare>` with `test_config --git-dir <bare>`.
The cleanup registered via `test_when_finished` now runs `test_unconfig
--git-dir`, which correctly uses `--unset-all` and handles exit code 5
(nothing to unset), matching the robust behavior that `test_unconfig`
has always provided.

Some diff hunks also contain adjacent `git -C` to `git --git-dir`
conversions for commands that appear in the same test and therefore the
same hunk as the `test_config` change.

This commit was produced with the help of Claude Opus 4.6 acting as a
sophisticated keyboard. It was told to replace `test_config -C` with
`test_config --git-dir` across bare-repository test sites, and to
convert any hand-rolled `test_when_finished` + `git config --unset`
pairs back into the `test_config` abstraction. The dominant pattern can
be verified:

    git show HEAD |
    sed 's/test_config --git-dir/test_config -C/g' |
    sed -ne '/^diff/,/@@/d' -e 's/^[-+]//p' |
    uniq -u

Residual output consists of adjacent `git -C` to `--git-dir`
conversions and, in t1400, a restructured `test_when_finished` that
splits config cleanup from reflog deletion.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When a test invokes `git -C <bare-repo>`, Git enters the directory and
discovers the bare repository implicitly. If `safe.bareRepository` is
set to `explicit`, this implicit discovery is refused and the test
fails. Regardless of whether the default changes in Git v3.0 or later,
it is the right thing to harden Git's own test suite so that it would
still pass under the stricter setting: the tests should demonstrate
the recommended practice, not rely on the lenient default.

Replace `git -C <bare>` with `git --git-dir=<bare>` throughout the
test suite wherever the target is a bare repository. The `--git-dir`
flag tells Git exactly where the repository is, bypassing the
discovery mechanism entirely. A handful of hunks additionally collapse
subshells that used `cd <bare> && git ...` into a single `git
--git-dir=<bare> ...` invocation, and several shared test helpers
(lib-commit-graph.sh, lib-diff-alternative.sh, lib-proto-disable.sh,
t5411/common-functions.sh) gain `--bare` or `--git-dir` parameters to
pass through to `git` internally.

Note that `--git-dir` does not change the working directory the way
`-C` does: relative paths in arguments are resolved from the original
directory, not from inside the repository. In this commit, no path
adjustments were necessary (those are handled in a separate commit).

This commit was produced with the help of Claude Opus 4.6 acting as a
sophisticated keyboard. It was told to replace every `git -C <bare>`
with `git --git-dir=<bare>` where the target is known to be a bare
repository, and to adjust helper functions in shared test libraries
accordingly.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
A companion commit replaced `git -C <bare>` with `git --git-dir=<bare>`
for simple cases. This commit handles the cases where that substitution
also requires adjusting relative paths in command arguments.

The `-C` flag changes the working directory before executing the
command, so arguments like `../foo` are resolved relative to the
repository. The `--git-dir` flag does not change the working directory,
so the same path must be expressed relative to the original directory
instead, e.g. `foo` rather than `../foo`, or `c/.git` rather than
`../c/.git`. Each hunk in this commit is therefore specific to its
test, making the changes not amenable to a single mechanical
verification command.

This commit was produced with the help of Claude Opus 4.6 acting as a
sophisticated keyboard. It was told to replace `git -C <bare>` with
`git --git-dir=<bare>` and to adjust every relative path that the `-C`
flag would have resolved differently. The affected files are:
t0001-init.sh, t0003-attributes.sh, t1091-sparse-checkout-builtin.sh,
t2400-worktree-add.sh, t2402-worktree-list.sh, t3200-branch.sh,
t5504-fetch-receive-strict.sh, t6020-bundle-misc.sh, t6500-gc.sh,
t7412-submodule-absorbgitdirs.sh, and t9003-help-autocorrect.sh.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Some tests need to `cd` into a bare repository rather than address it
from outside, typically because they create files there, inspect the
object store, or call helpers that assume they are already inside the
repository. For these, the `--git-dir` approach used by the companion
commits is not applicable: the test genuinely needs its working
directory to be inside the repository.

The solution is to set and export `GIT_DIR=.` immediately after
entering the bare repository. This tells Git explicitly where the
repository is and prevents the implicit discovery that
`safe.bareRepository=explicit` would reject. Should the default ever
change (Git v3.0 is the most likely candidate), these tests will
continue to work without modification.

A few cases deserve special mention. In t1400-update-ref.sh, the test
framework variable `$bare` points to the bare repository and the test
sets `GIT_DIR=$bare` with a `cd $bare` plus a later `cd -` and
`sane_unset GIT_DIR` for cleanup. In t2400-worktree-add.sh, some
subshells use the shorter inline form `GIT_DIR=. git worktree add ...`
where only a single command needs the override. In
t5411/common-functions.sh, the change affects the shared setup used by
the entire proc-receive test family. In lib-proto-disable.sh and
lib-bitmap.sh, the change is inside shared test library functions that
are sourced by multiple test scripts.

This commit was produced with the help of Claude Opus 4.6 acting as a
sophisticated keyboard. It was told to add `GIT_DIR=. && export
GIT_DIR &&` after every `cd <bare> &&` inside a subshell that runs Git
commands on a bare repository. The dominant pattern can be verified:

    git show HEAD |
    sed 's/ GIT_DIR=\. && export GIT_DIR &&//' |
    sed -ne '/^diff/,/@@/d' -e 's/^[-+]//p' |
    uniq -u

Residual output corresponds to structural adjustments (subshell
changes, inline `GIT_DIR=.` prefix form, cleanup via `sane_unset`) and
adjacent `git -C` to `--git-dir` conversions that share the same hunk.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Several tests enter the `.git` directory (or a bare repository)
directly via `cd .git` or `cd bare.git` and then run Git commands from
within. When `safe.bareRepository` is set to `explicit`, these commands
fail because Git considers the implicitly-discovered bare repository
unsafe. A future change, most likely timed for Git v3.0, intends to
make `explicit` the default.

The naive fix of setting `GIT_DIR=.` does not work here because it
changes worktree detection: Git would no longer see the parent as a
working tree, breaking tests that specifically verify "this operation
requires a work tree" (such as `git reset --hard` inside `.git`).

Instead, opt into the current default explicitly via the
`GIT_CONFIG_PARAMETERS` environment variable, following existing
precedent in t0033-safe-directory.sh. Each affected subshell exports
`GIT_CONFIG_PARAMETERS="'safe.bareRepository=all'"` so that the
bare-repository safety check passes while preserving the original
worktree semantics.

This commit was produced with the help of Claude Opus 4.6 acting as a
sophisticated keyboard. It was told to add
` && export GIT_CONFIG_PARAMETERS="${SQ}safe.bareRepository=all${SQ}"`
after every `cd .git` or `cd bare.git` line inside a subshell that runs
Git commands.

The purely mechanical nature of the change can be verified by stripping
the addition from the diff and confirming that no lines actually changed:

    git show HEAD |
    sed 's/ && export GIT_CONFIG_PARAMETERS=.*$//' |
    sed -ne '/^diff/,/@@/d' -e 's/^[-+]//p' |
    uniq -u

An empty output confirms that every hunk is the same mechanical
insertion.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
When an attacker can convince a user to clone a crafted repository that
contains an embedded bare repository with malicious hooks, any Git
command the user runs after entering that subdirectory will discover the
bare repository and execute the hooks. The user does not even need to run
a Git command explicitly: many shell prompts run `git status` in the
background to display branch and dirty state information, and `git
status` in turn may invoke the fsmonitor hook if so configured, making
the user vulnerable the moment they `cd` into the directory. The
safe.bareRepository configuration variable (introduced in 8959555
(setup_git_directory(): add an owner check for the top-level directory,
2022-03-02)) already provides protection against this attack vector by
allowing users to set it to "explicit", but the default remained "all"
for backwards compatibility.

Since Git 3.0 is the natural point to change defaults to safer values,
flip the default from "all" to "explicit" when built with
WITH_BREAKING_CHANGES. This means Git will refuse to work with bare
repositories that are discovered implicitly by walking up the directory
tree. Bare repositories specified via --git-dir or GIT_DIR continue to
work, and directories that look like .git, worktrees, or submodule
directories are unaffected (the existing is_implicit_bare_repo()
whitelist handles those cases).

Users who rely on implicit bare repository discovery can restore the
previous behavior by setting safe.bareRepository=all in their global or
system configuration.

The test for the "safe.bareRepository in the repository" scenario needed
a more involved fix: it writes a safe.bareRepository=all entry into the
bare repository's own config to verify that repo-local config does not
override the protected (global) setting. Previously, test_config -C was
used to write that entry, but its cleanup runs git -C <bare-repo> config
--unset, which itself fails when the default is "explicit" and the
global config has already been cleaned up. Switching to direct git config
--file access avoids going through repository discovery entirely.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@dscho dscho force-pushed the make-safe.bareRepositories-the-default-in-git-3.0 branch from 54b6c08 to a30a801 Compare March 27, 2026 17:31
@gitgitgadget
Copy link
Copy Markdown

gitgitgadget bot commented Mar 27, 2026

There are issues in commit 8365979:
infra: test-lib-functions and test-tool --git-dir

  • Commit checks stopped - the message is too short
  • Commit not signed off

@gitgitgadget
Copy link
Copy Markdown

gitgitgadget bot commented Mar 27, 2026

There are issues in commit 9d4fafe:
category-H

  • Commit checks stopped - the message is too short
  • Commit not signed off

@gitgitgadget
Copy link
Copy Markdown

gitgitgadget bot commented Mar 27, 2026

There are issues in commit 9d973bb:
category-L

  • Commit checks stopped - the message is too short
  • Commit not signed off

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant