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