Signed 'char' conversion in 'JsonParser::consumeUnicode'
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.
# 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/aria2csrc/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:19Impact
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.
Source: aria2/aria2