Skip to content

Commit 1d69c1a

Browse files
committed
rn-133: add git-am and commit messages article
1 parent ecb8cce commit 1d69c1a

File tree

1 file changed

+237
-2
lines changed

1 file changed

+237
-2
lines changed

rev_news/drafts/edition-133.md

Lines changed: 237 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,244 @@ This edition covers what happened during the months of February and March 2026.
2525
### Reviews
2626
-->
2727

28-
<!---
2928
### Support
30-
-->
29+
30+
+ [git-am applies commit message diffs](https://lore.kernel.org/git/bcqvh7ahjjgzpgxwnr4kh3hfkksfruf54refyry3ha7qk7dldf@fij5calmscvm)
31+
32+
On February 6, 2026, Matthias Beyer forwarded to the Git mailing list a
33+
surprising warning that had just circulated on Mastodon:
34+
35+
> PSA: Did you know that it's **unsafe** to put code diffs into your
36+
> commit messages?
37+
>
38+
> Such diffs will be applied by patch(1) (also git-am(1)) as part of
39+
> the code change!
40+
>
41+
> This is how a sleep(1) made it into i3 4.25-2 in Debian unstable.
42+
43+
The incident had originated in the i3 window manager project, where a
44+
commit message contained an unindented diff for illustration purposes.
45+
When Debian packagers later applied the patch using `patch(1)`, the diff
46+
in the commit message was applied as actual code, sneaking a spurious
47+
`sleep(1)` call into the Debian unstable package. Matthias asked the
48+
list whether this was a known issue and whether it could be an attack
49+
vector.
50+
51+
To understand why this happens, it helps to know how `git-am` parses
52+
its input. When processing a patch email, it must split the stream into
53+
two parts: the commit message and the actual patch to apply. It does
54+
this by treating the first occurrence of any of the following lines as
55+
the boundary between the two:
56+
57+
- a line consisting of only three dashes (`---`),
58+
- a line beginning with `diff -`, or
59+
- a line beginning with `Index: `.
60+
61+
Everything before that boundary becomes the commit message; everything
62+
after is fed to the patch application machinery. Crucially, `git-am`
63+
scans from the top of the email, so the very first such line it
64+
encounters terminates the commit message regardless of whether that
65+
line was meant to be part of the message text.
66+
67+
This design dates back to the tool's origins. As Jeff King (also known
68+
as "Peff") quickly explained in reply to Matthias, `git-am` was
69+
originally designed to handle patches sent by all kinds of people, not
70+
just Git users. A contributor might have generated a diff with plain
71+
GNU `diff` and typed the rest of the email by hand, without any `---`
72+
separator. The tool was therefore intentionally permissive: it would
73+
find a `diff -` line anywhere in the email and treat it as the start
74+
of the patch. Peff demonstrated this with a live example. He fed `git
75+
am` a hand-typed email containing a GNU diff, and it produced the
76+
expected commit.
77+
78+
This historical context also explained why `git-am` is notoriously
79+
hard to fix: "I don't think there is a way to unambiguously parse the
80+
single-stream output that format-patch produces," Peff wrote, noting
81+
that he could find at least three earlier discussions of the same
82+
problem (in 2015, 2022, and 2024). The stream is simply ambiguous by
83+
design. Even the `---` marker itself cannot be used to robustly split
84+
things, since `---` on a line by itself is a valid diff hunk line
85+
indicating that the string `--` was removed from a file.
86+
87+
Matthias proposed parsing from the end of the email rather than from
88+
the top. Peff replied that this would still be ambiguous for the same
89+
reasons, and would introduce new corner cases.
90+
91+
Jacob Keller noted early on that the issue was certainly surprising but
92+
that he was unsure it constituted a security attack vector, since
93+
someone should be reading the commit message before applying. But
94+
Matthias pushed back: the whole point was that nobody realized the
95+
behavior was there. He called it "sheer luck" that it was only a
96+
`sleep(1)` and not something more malicious crafted as a diff in the
97+
commit message.
98+
99+
Florian Weimer wondered whether the `git-format-patch` output was
100+
really ambiguous, given that the patch section is normally preceded by
101+
a diffstat block. Peff replied that the diffstat is optional and is not
102+
even parsed by the receiving side at all.
103+
104+
Jakob Haufe added an important nuance: even if `git-am` were fixed to
105+
require indented diffs, it would only partially mitigate the problem,
106+
because `patch(1)` (which many distributions use to apply upstream
107+
fixes to packages) is even more permissive. It will strip a consistent
108+
level of indentation from diffs before applying them. He quoted the
109+
`patch(1)` manual page: "If the entire diff is indented by a
110+
consistent amount, [...] this is taken into account." The i3 incident
111+
had in fact been triggered by `patch(1)`, not `git-am`.
112+
113+
Kristoffer Haugsbakk synthesized this into a clear summary of the
114+
situation and immediately proposed documenting it.
115+
116+
Matthias also highlighted the broader applicability beyond email
117+
workflows: Linux distributions like NixOS routinely fetch patches
118+
directly from upstream Git repositories and apply them to packages
119+
using `patch(1)`. He noted that even after 15 years of using Git and
120+
being comfortable with email patch workflows, he himself had not known
121+
about this behavior.
122+
123+
Several directions were then explored to look for solutions.
124+
125+
Peff observed the irony that `git-format-patch` does have a `--attach`
126+
option which puts the message and the patch in separate MIME parts —
127+
making them unambiguous in principle. However, `git-mailinfo` (which
128+
powers `git-am` under the hood) decodes both parts into a single
129+
stream and still treats a `diff` line in the message part as the start
130+
of a patch. Fixing this would require careful surgery to avoid
131+
breaking the existing forgiving handling of patches received as a
132+
single attachment.
133+
134+
Patrick Steinhardt suggested that even if parsing cannot be made
135+
unambiguous, `git-am` could at least detect the ambiguity and bail by
136+
default with an `--accept-ambiguous-patch` override. Jacob Keller
137+
proposed going further: a new "unambiguous mode" where
138+
`git-format-patch` would produce output that new versions of `git-am`
139+
could distinguish unambiguously, while old versions would still handle
140+
the common case the same way as before.
141+
142+
Jacob had also sketched a concrete scheme: add a new unambiguous
143+
marker after the `---` separator, so that old versions of `git-am`
144+
would still cut at the `---` and ignore everything up to the diff, while
145+
new versions would wait for the new marker and correctly ignore any
146+
diff appearing before it. Since the new marker would come after `---`,
147+
it would not be inserted into the commit message when applied.
148+
149+
Peff replied that this was trickier than it sounded: the new marker
150+
would have to be something that could never appear legitimately in a
151+
commit message, and both sides would need to complain if they saw
152+
multiple markers. He explored further options: reversible quoting of
153+
`---` and `diff` lines in the commit message (analogous to the `>From`
154+
quoting used in mbox files), applied only when the message would
155+
otherwise be ambiguous. This way, if an older `git-am` received the
156+
mail, the worst case would be visible quoting in the commit message —
157+
ugly but readable. Junio Hamano, the Git maintainer, added another
158+
thought: refusing to accept unsigned patches at all.
159+
160+
Peff also proposed a simpler receiver-side improvement: a
161+
`git am --strict` mode that would always require a `---` separator
162+
before the diff, on the assumption that well-formatted patches from Git
163+
always have one. This would not help with diffs that legitimately
164+
appear before the `---`, but would eliminate the most common accidental
165+
cases.
166+
167+
None of these ideas led to an immediate implementation, as they all
168+
involve backward compatibility tradeoffs that would need careful
169+
thought.
170+
171+
On February 8, Kristoffer sent a documentation patch titled "doc: add
172+
caveat about roundtripping format-patch" which introduced a new
173+
`Documentation/format-patch-caveats.adoc` file explaining the
174+
behavior. The caveat was designed to be included in the documentation
175+
for `git-am`, `git-format-patch`, and `git-send-email`.
176+
177+
Junio reviewed
178+
[version 1](https://lore.kernel.org/git/format-patch_caveats.281@msgid.xyz)
179+
and offered a correction to the wording: rather than saying that an
180+
unindented diff in the commit message "will not only cut the message
181+
short but cause that very diff to be applied, along with the patch in
182+
the patch section," Junio noted that the outcome is not so
183+
deterministic. The diff in the commit message might get applied, or
184+
the patch machinery might trip on something and fail outright. He also
185+
flagged that the space after the `---` in the cover letter was
186+
inconsistent with the project's conventions.
187+
188+
Phillip Wood reviewed the patch and found the mention of
189+
`git-send-email` a bit distracting, since that command merely runs
190+
`git-format-patch` and does not do any formatting itself. He also
191+
suggested wording improvements: replacing "One might want to use [...]
192+
patch(1)" with "Given these limitations, one might be tempted to [...]".
193+
194+
Kristoffer incorporated all of this in
195+
[version 2](https://lore.kernel.org/git/V2_format-patch_caveats.34b@msgid.xyz),
196+
which dropped the `git-send-email` mention from the introductory
197+
paragraph (while keeping the CAVEATS section in its documentation, for
198+
users who encounter it there), removed example code blocks in favor of
199+
clearer prose, and used the list of message-terminating patterns
200+
already present in `git-am`'s documentation. Junio reviewed it and
201+
queued it with the comment "Nicely written."
202+
203+
A third version,
204+
[version 3](https://lore.kernel.org/git/pull.2220.v3.git.git.1772559813151.gitgitgadget@gmail.com),
205+
was submitted and received Junio's approval to go to `next`.
206+
207+
Meanwhile, Phillip had observed that since the parsing cannot be fixed,
208+
"perhaps we should update our sample `commit-msg` hook to reject
209+
messages that will cause problems." On February 7, he sent a 3-patch
210+
series titled "commit-msg.sample: reject messages that would confuse
211+
`git am`". The series:
212+
213+
1. Added a `.gitattributes` rule for sample hooks (which are shell
214+
scripts but have `.sample` extensions).
215+
2. Extended the sample `commit-msg` hook to scan the body of the commit
216+
message for unindented `diff -` and `Index: ` lines and reject the
217+
commit with a helpful error message.
218+
3. Added a further check to detect `---` separator lines in the message
219+
body, which would cause `git-am` to silently truncate the commit
220+
message.
221+
222+
Peff reacted with measured skepticism to patch 3 in
223+
[version 1](https://lore.kernel.org/git/cover.1770476279.git.phillip.wood@dunelm.org.uk):
224+
he and Junio both pointed out that they themselves sometimes use `---`
225+
intentionally in commit messages to add notes that will appear in the
226+
formatted patch email but not end up in the final commit message when
227+
applied. Junio explained the trick: "when I know what I want to write
228+
below the three-dash lines, I would commit with `---` and additional
229+
notes below it, so that I do not forget during format-patch. When the
230+
commit is turned into a patch email [...] `am` cuts at the first one,
231+
and `apply` knows that the garbage lines at front, including
232+
three-dash lines, do not matter until it sees `^diff`, this works out
233+
perfectly well."
234+
235+
Peff confirmed he used the same trick. Phillip, acknowledging that at
236+
least three developers relied on this behavior, decided to drop patch 3
237+
entirely, reducing the series from three patches to two, in
238+
[version 2](https://lore.kernel.org/git/cover.1770993281.git.phillip.wood@dunelm.org.uk).
239+
He also refined the diff detection in the body: the v2 correctly skips
240+
the first paragraph of the message (which becomes the email Subject
241+
header and so does not go through the patch boundary detection), skips
242+
lines below a scissors line, and handles the `core.commentChar` and
243+
`core.commentString` configuration options for determining which lines
244+
are comments. Junio reviewed version 2 with detailed questions about
245+
the scissors-line logic.
246+
247+
Kristoffer verified that version 2 worked with `git commit
248+
--cleanup=scissors --verbose` and was satisfied.
249+
250+
The discussion did not lead to a fundamental fix to the ambiguous
251+
parsing in `git-am`, which remains an open problem with no obvious
252+
backward-compatible solution. But it produced two concrete
253+
improvements that were accepted and are now in `master`: a CAVEATS
254+
section in the documentation for `git-am`, `git-format-patch`, and
255+
`git-send-email` spelling out exactly how commit messages can
256+
inadvertently interfere with patch application, and an enhanced sample
257+
`commit-msg` hook that rejects messages containing unindented diffs.
258+
259+
The thread also served as a useful reminder that this problem is not
260+
limited to email workflows: any project that generates patches from
261+
Git commits using `git-format-patch` and applies them with `patch(1)`
262+
or `git-am` is exposed to it. The practical advice for authors is
263+
simple: if you include diffs in commit messages for illustrative
264+
purposes, make sure to indent them consistently, and be aware that
265+
even that does not protect you from `patch(1)`.
31266

32267
## Developer Spotlight: Olamide Caleb Bello
33268

0 commit comments

Comments
 (0)