#2388·aria2

Signed 'char' conversion in 'JsonParser::consumeUnicode'

Author: 2rr0r4o3Created Aug 24, 2026Updated Aug 24, 2026

Summary

When the JSON parser re-encodes a \\\uXXXX escape as UTF-8, it stores the byte values into a char array. Any code point that needs two or more UTF-8 bytes produces a lead byte of 0xC0 or higher, which is not representable in a signed char, so the conversion changes the value.

Details

/* src/JsonParser.cc:622-645, JsonParser::consumeUnicode */
640          temp[0] = 0xC0u | (codepoint_ >> 6);
641          temp[1] = 0x80u | (codepoint_ & 0x3Fu);

codepoint_ is uint32_t and the operands are unsigned, so the right-hand side of each assignment is unsigned int. temp is a char array. For any code point at or above U+0080 the value stored at :640 is at least 0xC0.

Same class as the util::fromHex issue, but the entry point is wider. fromHex needs a hex string in a metalink or a magnet link; this one is on the path every JSON-RPC request body takes, through HttpServerBodyCommand::execute -> ValueBaseDiskWriter<JsonParser>::writeData -> JsonParser::parseUpdate.

A fix belongs at :640. Making temp an unsigned char array, or casting on the way in, keeps the same bytes and removes the implementation defined step.

PoC

Requires the implicit-conversion checks, which -fsanitize=undefined does not include. Build as in the bittorrent::processRootDictionary case (-fsanitize=implicit-unsigned-integer-truncation,implicit-signed-integer-truncation,implicit-integer-sign-change plus the ignorelist that silences bitfield.h, base64.h, the util.h byte helpers and deps/). With that ignorelist a well-formed run reports nothing, so every hit is signal.

python
# rpc.py :  drives aria2's JSON-RPC endpoint with a GID string containing bytes >= 0x80
import json, subprocess, sys, tempfile, time, urllib.request, os
BIN, PORT = sys.argv[1], 18402
w = tempfile.mkdtemp()
log = open(os.path.join(w, "a.log"), "w+b")
p = subprocess.Popen([BIN, "--enable-rpc", "--rpc-listen-all=false",
                      "--rpc-listen-port=%d" % PORT, "--dir=" + w, "--no-conf=true",
                      "--console-log-level=warn", "--summary-interval=0"],
                     stdout=log, stderr=subprocess.STDOUT)

def rpc(method, params):
    b = json.dumps({"jsonrpc": "2.0", "id": "1", "method": method,
                    "params": params}).encode()
    q = urllib.request.Request("http://127.0.0.1:%d/jsonrpc" % PORT, data=b,
                               headers={"Content-Type": "application/json"})
    try:
        return urllib.request.urlopen(q, timeout=5).read()
    except Exception as e:
        return str(e).encode()

for _ in range(80):
    if b"result" in rpc("aria2.getVersion", []):
        break
    time.sleep(0.25)

gid = bytes([0xff, 0xfe]).decode("latin-1")          # non-ASCII, so JSON encodes it as UTF-8
for m in ("aria2.tellStatus", "aria2.getFiles", "aria2.getOption", "aria2.getUris"):
    rpc(m, [gid])
time.sleep(1)
p.terminate(); p.wait(timeout=20)
log.flush(); log.seek(0)
print(log.read().decode("utf8", "replace"))
UBSAN_OPTIONS=print_stacktrace=1 python3 rpc.py ./src/aria2c
src/JsonParser.cc:640:19: runtime error: implicit conversion from type 'unsigned int'
    of value 195 (32-bit, unsigned) to type 'char' changed the value to -61 (8-bit, signed)
    #0 aria2::json::JsonParser::consumeUnicode(char)  src/JsonParser.cc:640:19
    #1 aria2::json::JsonParser::parseUpdate(char const*, unsigned long)
                                                      src/JsonParser.cc:343:11
    #2 aria2::ValueBaseDiskWriter<aria2::json::JsonParser>::writeData(...)
                                                      src/ValueBaseDiskWriter.h
    #3 aria2::HttpServer::receiveBody()               src/HttpServer.cc:244:16
    #4 aria2::HttpServerBodyCommand::execute()        src/HttpServerBodyCommand.cc:187:24
    [...]
src/JsonParser.cc:641:19: runtime error: implicit conversion from type 'unsigned int'
    of value 191 (32-bit, unsigned) to type 'char' changed the value to -65 (8-bit, signed)
SUMMARY: UndefinedBehaviorSanitizer: implicit-integer-sign-change src/JsonParser.cc:640:19

Impact

Implementation-defined integer conversion for unauthenticated remote input. In this case, just like similar cases, I believe it is appropriate to treat this as a type handling bug.