commit: handle large commit messages in utf8 verification

Running t4205 under UBSan with the EXPENSIVE prereq enabled triggers an error when we try to create a commit message that is over 2GB: commit.c:1574:6: runtime error: signed integer overflow: -2147483648 - 1 cannot be represented in type 'int' The problem is that find_invalid_utf8() is not prepared to handle large buffers, as it uses an "int" to represent buffer sizes and offsets. We can fix this with a few changes: 1. We'll take in "len" as a size_t (which is what the caller has anyway, since it's working with a strbuf). 2. We need to return a size_t to give the offset to the invalid utf8, but we also need a sentinel value for "no invalid value" (previously "-1"). Let's split these to return a bool for "found invalid utf8" and then pass back the offset as an out-parameter. We'll switch the function name to match the new semantics. 3. The caller in verify_utf8() uses a "long" to store buffer positions, which is a bit funny. This goes back to 08a94a145c (commit/commit-tree: correct latin1 to utf-8, 2012-06-28) and is perhaps trying to match our use of "unsigned long" for object sizes (though we don't care about it ever becoming negative here). This should be a size_t, too, as some platforms (like Windows) still use a 32-bit long on machines with 64-bit pointers. 4. The "bytes" field within find_invalid_utf() does not have range problems. It is the number of bytes the utf8 sequence claims to have, so is limited by how many bits can be set in a single 8-bit byte. However, if we leave it as an "int" then the compiler will complain about the sign mismatch when comparing it to "len". So let's make it unsigned, too. All of this is a little silly, of course, because 2GB text commit messages are clearly nonsense. So we might consider rejecting them outright, but it is easy enough to make these helper functions more robust in the meantime. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed May 15, 2026 at 22:23 UTC 65ea197dca35363e7ee312620e5484473f95bbb4
1 file changed +15 -16
commit.c
+15 -16
@@ -1558,16 +1558,16 @@ int commit_tree(const char *msg, size_t msg_len, const struct object_id *tree,
1558 return result;
1559 }
1560
1561 -static int find_invalid_utf8(const char *buf, int len)
1561 +static bool has_invalid_utf8(const char *buf, size_t len, size_t *bad_offset)
1562 {
1563 - int offset = 0;
1563 + size_t offset = 0;
1564 static const unsigned int max_codepoint[] = {
1565 0x7f, 0x7ff, 0xffff, 0x10ffff
1566 };
1567
1568 while (len) {
1569 unsigned char c = *buf++;
1570 - int bytes, bad_offset;
1570 + unsigned bytes;
1571 unsigned int codepoint;
1572 unsigned int min_val, max_val;
1573
@@ -1578,7 +1578,7 @@ static int find_invalid_utf8(const char *buf, int len)
1578 if (c < 0x80)
1579 continue;
1580
1581 - bad_offset = offset-1;
1581 + *bad_offset = offset-1;
1582
1583 /*
1584 * Count how many more high bits set: that's how
@@ -1595,11 +1595,11 @@ static int find_invalid_utf8(const char *buf, int len)
1595 * codepoints beyond U+10FFFF, which are guaranteed never to exist.
1596 */
1597 if (bytes < 1 || 3 < bytes)
1598 - return bad_offset;
1598 + return true;
1599
1600 /* Do we *have* that many bytes? */
1601 if (len < bytes)
1602 - return bad_offset;
1602 + return true;
1603
1604 /*
1605 * Place the encoded bits at the bottom of the value and compute the
@@ -1617,23 +1617,23 @@ static int find_invalid_utf8(const char *buf, int len)
1617 codepoint <<= 6;
1618 codepoint |= *buf & 0x3f;
1619 if ((*buf++ & 0xc0) != 0x80)
1620 - return bad_offset;
1620 + return true;
1621 } while (--bytes);
1622
1623 /* Reject codepoints that are out of range for the sequence length. */
1624 if (codepoint < min_val || codepoint > max_val)
1625 - return bad_offset;
1625 + return true;
1626 /* Surrogates are only for UTF-16 and cannot be encoded in UTF-8. */
1627 if ((codepoint & 0x1ff800) == 0xd800)
1628 - return bad_offset;
1628 + return true;
1629 /* U+xxFFFE and U+xxFFFF are guaranteed non-characters. */
1630 if ((codepoint & 0xfffe) == 0xfffe)
1631 - return bad_offset;
1631 + return true;
1632 /* So are anything in the range U+FDD0..U+FDEF. */
1633 if (codepoint >= 0xfdd0 && codepoint <= 0xfdef)
1634 - return bad_offset;
1634 + return true;
1635 }
1636 - return -1;
1636 + return false;
1637 }
1638
1639 /*
@@ -1645,15 +1645,14 @@ static int find_invalid_utf8(const char *buf, int len)
1645 static int verify_utf8(struct strbuf *buf)
1646 {
1647 int ok = 1;
1648 - long pos = 0;
1648 + size_t pos = 0;
1649
1650 for (;;) {
1651 - int bad;
1651 + size_t bad;
1652 unsigned char c;
1653 unsigned char replace[2];
1654
1655 - bad = find_invalid_utf8(buf->buf + pos, buf->len - pos);
1656 - if (bad < 0)
1655 + if (!has_invalid_utf8(buf->buf + pos, buf->len - pos, &bad))
1656 return ok;
1657 pos += bad;
1658 ok = 0;