Merge branch 'bc/restrict-hex-to-lowercase' into seen

The parser for hex object names has been updated to reject uppercase
hexadecimal characters when running in the breaking changes mode, in
preparation for Git 3.0.

* bc/restrict-hex-to-lowercase:
  hex: allow only lowercase object IDs in breaking changes mode
  object-name: use hexval
  hex: label usages of hex parsing for object IDs
  hex: make hex_to_bytes accept kind of hex to use
  hex: allow specifying hex type with hex2chr
  hex: add functionality for lowercase-only hex
seen
Junio C Hamano 2026-08-31 13:53:26 -07:00
commit e605b87604
19 changed files with 90 additions and 38 deletions

View File

@ -171,6 +171,11 @@ JGit, libgit2 and Gitoxide need to support it.
matches the default branch name used in new repositories by many of the matches the default branch name used in new repositories by many of the
big Git forges. big Git forges.


* Git will accept hex object IDs only in lowercase. The fact that Git has
historically allowed uppercase characters in hex object IDs has been the
source of a variety of bugs and security problems in software using Git. We
don't expect most users to notice any change.

* Git will require Rust as a mandatory part of the build process. While Git * Git will require Rust as a mandatory part of the build process. While Git
already started to adopt Rust in Git 2.49, all parts written in Rust are already started to adopt Rust in Git 2.49, all parts written in Rust are
optional for the time being. This includes: optional for the time being. This includes:

View File

@ -1866,7 +1866,7 @@ static void repack_local_links(void)
while (strbuf_getline_lf(&line, out) != EOF) { while (strbuf_getline_lf(&line, out) != EOF) {
unsigned char binary[GIT_MAX_RAWSZ]; unsigned char binary[GIT_MAX_RAWSZ];
if (line.len != the_hash_algo->hexsz || if (line.len != the_hash_algo->hexsz ||
!hex_to_bytes(binary, line.buf, line.len)) !hex_to_bytes(binary, line.buf, line.len, HEX_KIND_MIXED))
die(_("index-pack: Expecting full hex object ID lines only from pack-objects.")); die(_("index-pack: Expecting full hex object ID lines only from pack-objects."));


/* /*

View File

@ -72,7 +72,7 @@ static int get_hex_color(const char **inp, int width, unsigned char *out)
unsigned int val; unsigned int val;


assert(width == 1 || width == 2); assert(width == 1 || width == 2);
val = (hexval(in[0]) << 4) | hexval(in[width - 1]); val = (hexval(in[0], HEX_KIND_MIXED) << 4) | hexval(in[width - 1], HEX_KIND_MIXED);
if (val & ~0xff) if (val & ~0xff)
return -1; return -1;
*inp += width; *inp += width;

View File

@ -112,7 +112,7 @@ static void loose_objs_stats(struct strbuf *buf, const char *path)
while ((e = readdir_skip_dot_and_dotdot(dir)) != NULL) while ((e = readdir_skip_dot_and_dotdot(dir)) != NULL)
if (get_dtype(e, &count_path, 0) == DT_DIR && if (get_dtype(e, &count_path, 0) == DT_DIR &&
strlen(e->d_name) == 2 && strlen(e->d_name) == 2 &&
!hex_to_bytes(&c, e->d_name, 1)) { !hex_to_bytes(&c, e->d_name, 1, HEX_KIND_OID)) {
strbuf_setlen(&count_path, base_path_len); strbuf_setlen(&count_path, base_path_len);
strbuf_addf(&count_path, "%s/", e->d_name); strbuf_addf(&count_path, "%s/", e->d_name);
total += (count = count_files(&count_path)); total += (count = count_files(&count_path));

View File

@ -36,10 +36,45 @@ const signed char hexval_table[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, /* f8-ff */ -1, -1, -1, -1, -1, -1, -1, -1, /* f8-ff */
}; };


int hex_to_bytes(unsigned char *binary, const char *hex, size_t len) const signed char hexval_lc_table[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, /* 00-07 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 08-0f */
-1, -1, -1, -1, -1, -1, -1, -1, /* 10-17 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 18-1f */
-1, -1, -1, -1, -1, -1, -1, -1, /* 20-27 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 28-2f */
0, 1, 2, 3, 4, 5, 6, 7, /* 30-37 */
8, 9, -1, -1, -1, -1, -1, -1, /* 38-3f */
-1, -1, -1, -1, -1, -1, -1, -1, /* 40-47 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 48-4f */
-1, -1, -1, -1, -1, -1, -1, -1, /* 50-57 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 58-5f */
-1, 10, 11, 12, 13, 14, 15, -1, /* 60-67 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 68-67 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 70-77 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 78-7f */
-1, -1, -1, -1, -1, -1, -1, -1, /* 80-87 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 88-8f */
-1, -1, -1, -1, -1, -1, -1, -1, /* 90-97 */
-1, -1, -1, -1, -1, -1, -1, -1, /* 98-9f */
-1, -1, -1, -1, -1, -1, -1, -1, /* a0-a7 */
-1, -1, -1, -1, -1, -1, -1, -1, /* a8-af */
-1, -1, -1, -1, -1, -1, -1, -1, /* b0-b7 */
-1, -1, -1, -1, -1, -1, -1, -1, /* b8-bf */
-1, -1, -1, -1, -1, -1, -1, -1, /* c0-c7 */
-1, -1, -1, -1, -1, -1, -1, -1, /* c8-cf */
-1, -1, -1, -1, -1, -1, -1, -1, /* d0-d7 */
-1, -1, -1, -1, -1, -1, -1, -1, /* d8-df */
-1, -1, -1, -1, -1, -1, -1, -1, /* e0-e7 */
-1, -1, -1, -1, -1, -1, -1, -1, /* e8-ef */
-1, -1, -1, -1, -1, -1, -1, -1, /* f0-f7 */
-1, -1, -1, -1, -1, -1, -1, -1, /* f8-ff */
};

int hex_to_bytes(unsigned char *binary, const char *hex, size_t len, enum hexkind kind)
{ {
for (; len; len--, hex += 2) { for (; len; len--, hex += 2) {
unsigned int val = (hexval(hex[0]) << 4) | hexval(hex[1]); unsigned int val = (hexval(hex[0], kind) << 4) | hexval(hex[1], kind);


if (val & ~0xff) if (val & ~0xff)
return -1; return -1;

View File

@ -1,20 +1,32 @@
#ifndef HEX_LL_H #ifndef HEX_LL_H
#define HEX_LL_H #define HEX_LL_H


enum hexkind {
HEX_KIND_MIXED = 0,
HEX_KIND_LOWER = 1,
};

#ifdef WITH_BREAKING_CHANGES
#define HEX_KIND_OID HEX_KIND_LOWER
#else
#define HEX_KIND_OID HEX_KIND_MIXED
#endif

extern const signed char hexval_table[256]; extern const signed char hexval_table[256];
static inline unsigned int hexval(unsigned char c) extern const signed char hexval_lc_table[256];
static inline unsigned int hexval(unsigned char c, enum hexkind kind)
{ {
return hexval_table[c]; return kind == HEX_KIND_MIXED ? hexval_table[c] : hexval_lc_table[c];
} }


/* /*
* Convert two consecutive hexadecimal digits into a char. Return a * Convert two consecutive hexadecimal digits into a char. Return a
* negative value on error. Don't run over the end of short strings. * negative value on error. Don't run over the end of short strings.
*/ */
static inline int hex2chr(const char *s) static inline int hex2chr(const char *s, enum hexkind kind)
{ {
unsigned int val = hexval(s[0]); unsigned int val = hexval(s[0], kind);
return (val & ~0xf) ? val : (val << 4) | hexval(s[1]); return (val & ~0xf) ? val : (val << 4) | hexval(s[1], kind);
} }


/* /*
@ -22,6 +34,6 @@ static inline int hex2chr(const char *s)
* values to `binary` as `len` bytes. Return 0 on success, or -1 if * values to `binary` as `len` bytes. Return 0 on success, or -1 if
* the input does not consist of hex digits). * the input does not consist of hex digits).
*/ */
int hex_to_bytes(unsigned char *binary, const char *hex, size_t len); int hex_to_bytes(unsigned char *binary, const char *hex, size_t len, enum hexkind kind);


#endif #endif

2
hex.c
View File

@ -9,7 +9,7 @@ static int get_hash_hex_algop(const char *hex, unsigned char *hash,
const struct git_hash_algo *algop) const struct git_hash_algo *algop)
{ {
for (size_t i = 0; i < algop->rawsz; i++) { for (size_t i = 0; i < algop->rawsz; i++) {
int val = hex2chr(hex); int val = hex2chr(hex, HEX_KIND_OID);
if (val < 0) if (val < 0)
return -1; return -1;
*hash++ = val; *hash++ = val;

View File

@ -1031,12 +1031,13 @@ static int get_oid_hex_from_objpath(const char *path, struct object_id *oid)
if (strlen(path) != the_hash_algo->hexsz + 1) if (strlen(path) != the_hash_algo->hexsz + 1)
return -1; return -1;


if (hex_to_bytes(oid->hash, path, 1)) if (hex_to_bytes(oid->hash, path, 1, HEX_KIND_OID))
return -1; return -1;
path += 2; path += 2;
path++; /* skip '/' */ path++; /* skip '/' */


return hex_to_bytes(oid->hash + 1, path, the_hash_algo->rawsz - 1); return hex_to_bytes(oid->hash + 1, path, the_hash_algo->rawsz - 1,
HEX_KIND_OID);
} }


static void process_ls_object(struct remote_ls_ctx *ls) static void process_ls_object(struct remote_ls_ctx *ls)

View File

@ -396,7 +396,7 @@ static int decode_q_segment(struct strbuf *out, const struct strbuf *q_seg,
int ch, d = *in; int ch, d = *in;
if (d == '\n' || !d) if (d == '\n' || !d)
break; /* drop trailing newline */ break; /* drop trailing newline */
ch = hex2chr(in); ch = hex2chr(in, HEX_KIND_MIXED);
if (ch >= 0) { if (ch >= 0) {
strbuf_addch(out, ch); strbuf_addch(out, ch);
in += 2; in += 2;

View File

@ -428,7 +428,7 @@ static void load_subtree(struct notes_tree *t, struct leaf_node *subtree,
goto handle_non_note; goto handle_non_note;


if (hex_to_bytes(object_oid.hash + prefix_len, entry.path, if (hex_to_bytes(object_oid.hash + prefix_len, entry.path,
hashsz - prefix_len)) hashsz - prefix_len, HEX_KIND_MIXED))
goto handle_non_note; /* entry.path is not a SHA1 */ goto handle_non_note; /* entry.path is not a SHA1 */


memset(object_oid.hash + hashsz, 0, GIT_MAX_RAWSZ - hashsz); memset(object_oid.hash + hashsz, 0, GIT_MAX_RAWSZ - hashsz);
@ -442,7 +442,8 @@ static void load_subtree(struct notes_tree *t, struct leaf_node *subtree,
/* internal nodes must be trees */ /* internal nodes must be trees */
goto handle_non_note; goto handle_non_note;


if (hex_to_bytes(object_oid.hash + len++, entry.path, 1)) if (hex_to_bytes(object_oid.hash + len++, entry.path, 1,
HEX_KIND_OID))
goto handle_non_note; /* entry.path is not a SHA1 */ goto handle_non_note; /* entry.path is not a SHA1 */


/* /*

View File

@ -1081,7 +1081,7 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,
strbuf_add(path, de->d_name, namelen); strbuf_add(path, de->d_name, namelen);
if (namelen == algop->hexsz - 2 && if (namelen == algop->hexsz - 2 &&
!hex_to_bytes(oid.hash + 1, de->d_name, !hex_to_bytes(oid.hash + 1, de->d_name,
algop->rawsz - 1)) { algop->rawsz - 1, HEX_KIND_OID)) {
oid_set_algo(&oid, algop); oid_set_algo(&oid, algop);
memset(oid.hash + algop->rawsz, 0, memset(oid.hash + algop->rawsz, 0,
GIT_MAX_RAWSZ - algop->rawsz); GIT_MAX_RAWSZ - algop->rawsz);

View File

@ -236,17 +236,10 @@ static int parse_oid_prefix(const char *name, int len,
{ {
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
unsigned char c = name[i]; unsigned char c = name[i];
unsigned char val; int val = hexval(c, HEX_KIND_OID);
if (c >= '0' && c <= '9') {
val = c - '0'; if (val < 0)
} else if (c >= 'a' && c <= 'f') {
val = c - 'a' + 10;
} else if (c >= 'A' && c <='F') {
val = c - 'A' + 10;
c -= 'A' - 'a';
} else {
return -1; return -1;
}


if (hex_out) if (hex_out)
hex_out[i] = c; hex_out[i] = c;

View File

@ -378,10 +378,10 @@ int packet_length(const char lenbuf_hex[4], size_t size)
{ {
if (size < 4) if (size < 4)
BUG("buffer too small"); BUG("buffer too small");
return hexval(lenbuf_hex[0]) << 12 | return hexval(lenbuf_hex[0], HEX_KIND_MIXED) << 12 |
hexval(lenbuf_hex[1]) << 8 | hexval(lenbuf_hex[1], HEX_KIND_MIXED) << 8 |
hexval(lenbuf_hex[2]) << 4 | hexval(lenbuf_hex[2], HEX_KIND_MIXED) << 4 |
hexval(lenbuf_hex[3]); hexval(lenbuf_hex[3], HEX_KIND_MIXED);
} }


static const char *find_packfile_uri_path(const char *buffer) static const char *find_packfile_uri_path(const char *buffer)

View File

@ -3636,7 +3636,7 @@ static void append_literal(const char *cp, const char *ep, struct ref_formatting
if (cp[1] == '%') if (cp[1] == '%')
cp++; cp++;
else { else {
int ch = hex2chr(cp + 1); int ch = hex2chr(cp + 1, HEX_KIND_MIXED);
if (0 <= ch) { if (0 <= ch) {
strbuf_addch(s, ch); strbuf_addch(s, ch);
cp += 3; cp += 3;

View File

@ -457,7 +457,7 @@ size_t strbuf_expand_literal(struct strbuf *sb, const char *placeholder)
return 1; return 1;
case 'x': case 'x':
/* %x00 == NUL, %x0a == LF, etc. */ /* %x00 == NUL, %x0a == LF, etc. */
ch = hex2chr(placeholder + 1); ch = hex2chr(placeholder + 1, HEX_KIND_MIXED);
if (ch < 0) if (ch < 0)
return 0; return 0;
strbuf_addch(sb, ch); strbuf_addch(sb, ch);

View File

@ -60,6 +60,11 @@ test_expect_success 'works with one good rev' '
test "$rev_head" = "$HASH4" test "$rev_head" = "$HASH4"
' '


test_expect_success WITH_BREAKING_CHANGES 'rejects uppercase revs' '
UC_HASH=$(echo "$HASH1" | tr a-f A-F) &&
test_must_fail git rev-parse --verify "$UC_HASH"
'

test_expect_success 'fails with any bad rev or many good revs' ' test_expect_success 'fails with any bad rev or many good revs' '
test_must_fail git rev-parse --verify 2>error && test_must_fail git rev-parse --verify 2>error &&
test_grep "single revision" error && test_grep "single revision" error &&

View File

@ -349,7 +349,7 @@ test_expect_success 'verify after commit-graph-chain corruption (base)' '
test_must_fail git commit-graph verify 2>test_err && test_must_fail git commit-graph verify 2>test_err &&
grep -v "^+" test_err >err && grep -v "^+" test_err >err &&
test_grep "invalid commit-graph chain" err && test_grep "invalid commit-graph chain" err &&
corrupt_file "$graphdir/commit-graph-chain" 30 "A" && corrupt_file "$graphdir/commit-graph-chain" 30 "a" &&
test_must_fail git commit-graph verify 2>test_err && test_must_fail git commit-graph verify 2>test_err &&
grep -v "^+" test_err >err && grep -v "^+" test_err >err &&
test_grep "unable to find all commit-graph files" err test_grep "unable to find all commit-graph files" err
@ -364,7 +364,7 @@ test_expect_success 'verify after commit-graph-chain corruption (tip)' '
test_must_fail git commit-graph verify 2>test_err && test_must_fail git commit-graph verify 2>test_err &&
grep -v "^+" test_err >err && grep -v "^+" test_err >err &&
test_grep "invalid commit-graph chain" err && test_grep "invalid commit-graph chain" err &&
corrupt_file "$graphdir/commit-graph-chain" 70 "A" && corrupt_file "$graphdir/commit-graph-chain" 70 "b" &&
test_must_fail git commit-graph verify 2>test_err && test_must_fail git commit-graph verify 2>test_err &&
grep -v "^+" test_err >err && grep -v "^+" test_err >err &&
test_grep "unable to find all commit-graph files" err test_grep "unable to find all commit-graph files" err

2
url.c
View File

@ -62,7 +62,7 @@ static char *url_decode_internal(const char **query, int len,
} }


if (c == '%' && (len < 0 || len >= 3)) { if (c == '%' && (len < 0 || len >= 3)) {
int val = hex2chr(q + 1); int val = hex2chr(q + 1, HEX_KIND_MIXED);
if (0 < val) { if (0 < val) {
strbuf_addch(out, val); strbuf_addch(out, val);
q += 3; q += 3;

View File

@ -50,7 +50,7 @@ static int append_normalized_escapes(struct strbuf *buf,
if (ch == '%') { if (ch == '%') {
if (from_len < 2) if (from_len < 2)
return 0; return 0;
ch = hex2chr(from); ch = hex2chr(from, HEX_KIND_MIXED);
if (ch < 0) if (ch < 0)
return 0; return 0;
from += 2; from += 2;