Re: [RFC PATCH 6/6] hex: allow only lowercase object IDs in breaking changes mode
Michael Montalbo <[email protected]> Tue, 4 Aug 2026 20:09:53 -0700
| Newsgroups | org.kernel.vger.git |
|---|---|
| Message-ID | <CAC2QwmKS+ojHd31oagdHv1G3h=Sa-BttWpQLv49=kC=PC-1BTQ@mail.gmail.com> |
On Sun, Aug 2, 2026 at 3:10 PM brian m. carlson <[email protected]> wrote: > > Modern development effectively requires being clear and definitive about > what data is accepted and what is not, as well as what meaning is given > to the data that is accepted. > I agree with this idea, and the topic inspired me to explore how mixing upper and lowercase hex oids might be "abused" today. Interestingly, I found out it is possible to mix upper and lowercase formats within one oid. Depending on output path, Git either normalizes the casing or preserves the raw form stored. I didn't come up with a specific way to take advantage of this behavior yet, but one could imagine a scenario where downstream consumers of Git output each have their own way of parsing such an ambiguously formatted oid and behave in different ways that at best may cause confusion and at worst could be used maliciously. To reproduce a mixed case oid scenario: #!/bin/sh set -eu ( repo=$(mktemp -d); cd "$repo" export GIT_PAGER=cat git init -q git config user.name Tester && git config user.email [email protected] echo one >f && git add f && git commit -qm base echo two >f && git commit -qam child # Re-spell the child's parent OID with a mixed-case tail, re-store the # object. Nothing else about the commit changes. parent=$(git rev-parse HEAD^) upper=$(printf %s "$parent" | tr '[:lower:]' '[:upper:]') mixed=$(printf %s "$parent" | cut -c1-10)$(printf %s "$parent" | cut -c11- | tr '[:lower:]' '[:upper:]') git cat-file commit HEAD | sed "s/^parent .*/parent $mixed/" >crafted-obj crafted=$(git hash-object -w -t commit --stdin <crafted-obj) git update-ref refs/heads/mixed "$crafted" echo "== the two commit objects differ by one field, case only ==" git cat-file commit HEAD >canon-obj diff canon-obj crafted-obj || true echo echo "== they are distinct commits ==" printf 'canonical: %s\nmixed : %s\n' "$(git rev-parse HEAD)" "$crafted" echo echo "== both parent spellings resolve to the same object ==" git rev-parse "$parent" "$upper" echo echo "== the one parent is spelled two ways, by output path ==" printf 'raw (as stored) : '; git log -1 --pretty=raw mixed | sed -n 's/^parent //p' printf '%%P (normalized): '; git log -1 --format='%P' mixed ) produces: == the two commit objects differ by one field, case only == 2c2 < parent f3b4ff525d82ddcec0cc8597842c73b72e4c5aba --- > parent f3b4ff525d82DDCEC0CC8597842C73B72E4C5ABA == they are distinct commits == canonical: 52d41f6a261b49a18d38933b59a65f2dc919f9ac mixed : ed20e85251a37eb01009faaaa8fbc23baf8bdc72 == both parent spellings resolve to the same object == f3b4ff525d82ddcec0cc8597842c73b72e4c5aba f3b4ff525d82ddcec0cc8597842c73b72e4c5aba == the one parent is spelled two ways, by output path == raw (as stored) : f3b4ff525d82DDCEC0CC8597842C73B72E4C5ABA %P (normalized): f3b4ff525d82ddcec0cc8597842c73b72e4c5aba