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
This commit is contained in:
Junio C Hamano
2026-08-07 14:48:30 -07:00
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
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
already started to adopt Rust in Git 2.49, all parts written in Rust are
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) {
unsigned char binary[GIT_MAX_RAWSZ];
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."));
/*

View File

@@ -72,7 +72,7 @@ static int get_hex_color(const char **inp, int width, unsigned char *out)
unsigned int val;
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)
return -1;
*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)
if (get_dtype(e, &count_path, 0) == DT_DIR &&
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_addf(&count_path, "%s/", e->d_name);
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 */
};
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) {
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)
return -1;

View File

@@ -1,20 +1,32 @@
#ifndef 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];
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
* 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]);
return (val & ~0xf) ? val : (val << 4) | hexval(s[1]);
unsigned int val = hexval(s[0], kind);
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
* 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

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)
{
for (size_t i = 0; i < algop->rawsz; i++) {
int val = hex2chr(hex);
int val = hex2chr(hex, HEX_KIND_OID);
if (val < 0)
return -1;
*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)
return -1;
if (hex_to_bytes(oid->hash, path, 1))
if (hex_to_bytes(oid->hash, path, 1, HEX_KIND_OID))
return -1;
path += 2;
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)

View File

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

View File

@@ -428,7 +428,7 @@ static void load_subtree(struct notes_tree *t, struct leaf_node *subtree,
goto handle_non_note;
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 */
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 */
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 */
/*

View File

@@ -1071,7 +1071,7 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,
strbuf_add(path, de->d_name, namelen);
if (namelen == algop->hexsz - 2 &&
!hex_to_bytes(oid.hash + 1, de->d_name,
algop->rawsz - 1)) {
algop->rawsz - 1, HEX_KIND_OID)) {
oid_set_algo(&oid, algop);
memset(oid.hash + algop->rawsz, 0,
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++) {
unsigned char c = name[i];
unsigned char val;
if (c >= '0' && c <= '9') {
val = c - '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 {
int val = hexval(c, HEX_KIND_OID);
if (val < 0)
return -1;
}
if (hex_out)
hex_out[i] = c;

View File

@@ -378,10 +378,10 @@ int packet_length(const char lenbuf_hex[4], size_t size)
{
if (size < 4)
BUG("buffer too small");
return hexval(lenbuf_hex[0]) << 12 |
hexval(lenbuf_hex[1]) << 8 |
hexval(lenbuf_hex[2]) << 4 |
hexval(lenbuf_hex[3]);
return hexval(lenbuf_hex[0], HEX_KIND_MIXED) << 12 |
hexval(lenbuf_hex[1], HEX_KIND_MIXED) << 8 |
hexval(lenbuf_hex[2], HEX_KIND_MIXED) << 4 |
hexval(lenbuf_hex[3], HEX_KIND_MIXED);
}
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] == '%')
cp++;
else {
int ch = hex2chr(cp + 1);
int ch = hex2chr(cp + 1, HEX_KIND_MIXED);
if (0 <= ch) {
strbuf_addch(s, ch);
cp += 3;

View File

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

View File

@@ -60,6 +60,11 @@ test_expect_success 'works with one good rev' '
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_must_fail git rev-parse --verify 2>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 &&
grep -v "^+" test_err >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 &&
grep -v "^+" test_err >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 &&
grep -v "^+" test_err >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 &&
grep -v "^+" test_err >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)) {
int val = hex2chr(q + 1);
int val = hex2chr(q + 1, HEX_KIND_MIXED);
if (0 < val) {
strbuf_addch(out, val);
q += 3;

View File

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