Skip to content

plumbing: format/config, Reject unrepresentable subsection names when encoding - #2359

Open
r0h1tb wants to merge 2 commits into
go-git:mainfrom
r0h1tb:fix/config-encoder-newline-subsection
Open

r0h1tb wants to merge 2 commits into
go-git:mainfrom
r0h1tb:fix/config-encoder-newline-subsection

Conversation

@r0h1tb

@r0h1tb r0h1tb commented Sep 3, 2026 •

Copy link
Copy Markdown

Problem

git-config(1): subsection names "can contain any characters except newline and the null byte."

The encoder escapes only " and \, so either of those two characters is written raw into the section header and the result cannot be read back — by go-git or by git:

m := config.NewModules()
m.Unmarshal([]byte("[submodule \"a\\\nb\"]\n\tpath = p\n\turl = https://example.com/x.git\n"))

out, _ := m.Marshal()
// [submodule "a
// b"]
//     path = p
//     url = https://example.com/x.git

config.NewModules().Unmarshal(out) // 1:12: string not terminated

git config --file … --list on that same output: fatal: bad config line 1. With a NUL instead of the newline, go-git reports illegal character NUL and git silently truncates the name to a.

This is reachable from .gitmodules: submodule names come from the repository being operated on, and config.Modules.Marshal re-encodes them.

Fix

Return an error instead of writing a file nothing can parse.

SetConfig now marshals before it opens the config writer. The writer truncates the file, so with the old order that error left the repository with an empty config.

Escaping is not an option here. git drops a backslash before any character other than " and \, so \n reads back as the letter n and would quietly rename the subsection. git refuses to create such a key at all (error: invalid key (newline)), so the encoder does the same.

Tests

Two regression tests in encoder_test.go. Withdrawing the fix while keeping them reproduces the failure on main:

--- FAIL: TestEncoderSuite/TestEncodeNeverWritesUnreadableSubsectionHeader
    Error:    Received unexpected error: 1:12: string not terminated
    Messages: encoder wrote a config that the decoder cannot read:
              "[submodule \"a\nb\"]\n\turl = https://example.com/x.git\n"

TestSetConfigKeepsExistingConfigWhenEncodingFails covers the save path: with the old order the config is read back as "".

Rebased on main. go test ./... passes apart from the known plumbing/format/gitignore TestConformanceSuite failure (#2317). The root package and plumbing/transport/http hit network timeouts on my machine when the whole suite runs at once, and both pass when run on their own. golangci-lint reports 0 issues for both packages. Behaviour checked against git 2.50.1.

Scope

Section names and option keys have the same gap — they are written with no escaping or validation at all, so a space, tab, newline, ], " or \ in either also yields an unreadable file. Those aren't reachable from repository data the way subsection names are, so I left them out to keep this a focused fix; happy to extend it if you'd prefer them covered here.

Found with a decode/encode/decode round-trip fuzz target, not included in this PR.


AI disclosure (per AI_POLICY.md): this change was produced with an AI agent, Claude Opus 5 and Opus 5.5 via Claude Code, and both commits carry the Assisted-by: trailer. The reproduction, the git 2.50.1 comparisons, the baseline/after suite runs and the withdraw-the-fix check above were all actually executed, and the output quoted is real.

@pjbgf pjbgf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test to ensure that cfg.Marshal() is called before ConfigWriter(), so trying to save an invalid section don't leave a repository with an empty config file. Instead it should error and retain the existing config file.

r0h1tb added 2 commits October 5, 2026 19:37
… encoding.

git-config(1) states that subsection names "can contain any characters except
newline and the null byte". The encoder escaped only `"` and `\`, so either
character was written raw into the section header and produced a file that
could not be read back: go-git reports "string not terminated" / "illegal
character NUL", and git reports "bad config line" or silently truncates the
name at the NUL.

Escaping them is not an option, because git drops a backslash before any
character other than `"` and `\` — `\n` reads back as the letter "n", which
would quietly rename the subsection. git refuses to create such a key at all
("invalid key (newline)"), so do the same and return an error rather than
write a config that nothing can parse.

Reachable from .gitmodules, whose submodule names come from the repository
itself and are re-encoded by config.Modules.Marshal.

Assisted-by: Claude Opus 5 <[email protected]>
Signed-off-by: Rohit Behera <[email protected]>
SetConfig opened the config writer, which truncates the file, before
calling Marshal. Now that Marshal can fail on a subsection name git
cannot represent, a failed save left the repository with an empty
config. Marshal first, so the error is returned and the existing file
is kept.

Assisted-by: Claude Opus 5.5 <[email protected]>
Signed-off-by: Rohit Behera <[email protected]>
@r0h1tb
r0h1tb force-pushed the fix/config-encoder-newline-subsection branch from 6a14949 to cb4c1ed Compare October 5, 2026 14:46
@r0h1tb

r0h1tb commented Oct 5, 2026

Copy link
Copy Markdown
Author

Good catch, thanks. SetConfig opened the writer (which truncates the file) before calling Marshal, so with this change a submodule name with a newline in it would have left the repo with an empty config. It marshals first now, so it errors out and the existing file stays as it was.

Added TestSetConfigKeepsExistingConfigWhenEncodingFails for it. On the old order it fails with the config read back as "". Also rebased on main.

@r0h1tb
r0h1tb marked this pull request as ready for review October 5, 2026 14:46
@r0h1tb

r0h1tb commented Oct 5, 2026

Copy link
Copy Markdown
Author

The Windows failure looks unrelated: plumbing/transport exited with an access violation (0xc0000005, no Go panic output), and this PR doesn't touch that package. The same job passes on main and the other open PRs, so it probably just needs a re-run, which I can't trigger from here.

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.

2 participants