epee, common: pass unsigned char to <cctype> functions

The <cctype> functions require their argument to be representable as
unsigned char or to equal EOF. char is signed on the platforms Monero
targets, so every byte >= 0x80 reaches them negative, which is undefined
behaviour.

All six call sites take bytes that come from outside the process:

  hex_to_dec_2bytes()     percent-escapes in a payment URI
  http_client.h           the status line of an HTTP response
  updates.cpp             the hash field of a DNS update record

glibc and the macOS libc happen to tolerate -128..-1 because their tables
carry padding below zero, so this is quiet in practice on those, but it is
still out of contract and other libcs index without that cushion.

Cast to unsigned char at each call. The conversion is behaviour-preserving
for the ASCII these actually test: for a byte such as 0xC3, toupper()
returns it unchanged in the C locale, it narrows back to char, memchr()
misses it in the hex table, and hex_to_dec_2bytes() falls through to the
existing literal "%XY" output exactly as before.

epee::misc_utils::parse::isdigit/isalpha/isspace are separate char-taking
helpers and are already well defined, so they are untouched, as is
is_base64(), which already takes unsigned char.
This commit is contained in:
pucedoteth
2026-08-29 04:38:57 +02:00
parent 78fb311ebb
commit 1f58e91d60
3 changed files with 6 additions and 6 deletions
+3 -3
View File
@@ -662,14 +662,14 @@ namespace net_utils
ptr = end + 1;
CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line: " + m_header_cache + ", ptr: " << ptr);
ul = strtoul(ptr, &end, 10);
CHECK_AND_ASSERT_MES(ul <= INT_MAX && isblank(*end), false, "Invalid first response line: " + m_header_cache + ", ptr: " << ptr);
CHECK_AND_ASSERT_MES(ul <= INT_MAX && isblank(static_cast<unsigned char>(*end)), false, "Invalid first response line: " + m_header_cache + ", ptr: " << ptr);
m_response_info.m_http_ver_lo = ul;
ptr = end + 1;
while (isblank(*ptr))
while (isblank(static_cast<unsigned char>(*ptr)))
++ptr;
CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line: " + m_header_cache);
ul = strtoul(ptr, &end, 10);
CHECK_AND_ASSERT_MES(ul >= 100 && ul <= 999 && isspace(*end), false, "Invalid first response line: " + m_header_cache);
CHECK_AND_ASSERT_MES(ul >= 100 && ul <= 999 && isspace(static_cast<unsigned char>(*end)), false, "Invalid first response line: " + m_header_cache);
m_response_info.m_response_code = ul;
ptr = end;
// ignore the optional text, till the end
+2 -2
View File
@@ -65,8 +65,8 @@ namespace net_utils
std::string hex_to_dec_2bytes(const char *s)
{
const char *hex = get_hex_vals();
int i0 = get_index(hex, toupper(s[0]));
int i1 = get_index(hex, toupper(s[1]));
int i0 = get_index(hex, toupper(static_cast<unsigned char>(s[0])));
int i1 = get_index(hex, toupper(static_cast<unsigned char>(s[1])));
if (i0 < 0 || i1 < 0)
return std::string("%") + std::string(1, s[0]) + std::string(1, s[1]);
return std::string(1, i0 * 16 | i1);
+1 -1
View File
@@ -73,7 +73,7 @@ namespace tools
bool alnum = true;
for (auto c: fields[3])
if (!isalnum(c))
if (!isalnum(static_cast<unsigned char>(c)))
alnum = false;
if (fields[3].size() != 64 && !alnum)
{