Severity: High — memory safety, 1-byte heap out-of-bounds read in the in-tree JSON parser.
Affected function / file: json_parse_ex, src/json.c:307-311 (v3.3.6, commit 8ad3036).
Root cause
Classic length-check-before-pre-increment off-by-one. The guard permits exactly 4 bytes remaining after state.ptr, but the body performs 4 sequential *++state.ptr reads, which dereference ptr+1 … ptr+4. The fourth read lands one byte past end.
case 'u':
if (end - state.ptr < 4 ||
(uc_b1 = hex_value (*++ state.ptr)) == 0xFF ||
(uc_b2 = hex_value (*++ state.ptr)) == 0xFF ||
(uc_b3 = hex_value (*++ state.ptr)) == 0xFF ||
(uc_b4 = hex_value (*++ state.ptr)) == 0xFF) // <-- 1 past end
The guard should be <= 4 (equivalently < 5), or the reads should be sequenced against a pre-computed end pointer.
Reachability
json_parse_ex is reached from:
- Every
-c config.json invocation via read_jconf → json_parse_ex (all five binaries).
- The
ss-manager localhost UDP control channel, which accepts unauthenticated UDP commands (add, remove, ping) that embed a JSON payload. This is a pre-auth read of one heap byte on any host running ss-manager.
The leaked byte is immediately fed to hex_value() and compared to 0xFF, so it is not directly a control-flow primitive — but it is still a real heap OOB read on untrusted input, and the JSON parser is a fork of udp/json-parser which has had similar patches upstream that were never backported to this in-tree copy.
Reproduction
Minimal reproducer (30 bytes) — restore from base64:
echo 'eyI5MjhociRdZvgqMmVyXHVCMkZCJHZgclx1QjJE' | base64 -d > repro.bin
./fuzz_json repro.bin
ASan output:
==*==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x... READ of size 1
#0 json_parse_ex src/json.c:311:45
257 of 281 saved inputs from a 300 s libFuzzer round on json_parse_ex collapse to this root cause.
Suggested fix
Change the guard to <= 4:
if (end - state.ptr <= 4 || ...)
Or refactor to bulk-check then bulk-read:
if (state.ptr + 4 >= end) { /* error */ }
uc_b1 = hex_value(state.ptr[1]);
uc_b2 = hex_value(state.ptr[2]);
uc_b3 = hex_value(state.ptr[3]);
uc_b4 = hex_value(state.ptr[4]);
state.ptr += 4;
if (uc_b1 == 0xFF || uc_b2 == 0xFF || uc_b3 == 0xFF || uc_b4 == 0xFF) { /* error */ }
The same off-by-one pattern also appears at src/json.c:324-328 (UTF-16 surrogate branch, < 6) and at lines 604 / 621 / 635 (literals true / false / null) — worth fixing as a family in one commit.
Context
Found as part of an independent fuzzing audit of shadowsocks-libev v3.3.6.
Severity: High — memory safety, 1-byte heap out-of-bounds read in the in-tree JSON parser.
Affected function / file:
json_parse_ex,src/json.c:307-311(v3.3.6, commit8ad3036).Root cause
Classic length-check-before-pre-increment off-by-one. The guard permits exactly 4 bytes remaining after
state.ptr, but the body performs 4 sequential*++state.ptrreads, which dereferenceptr+1 … ptr+4. The fourth read lands one byte pastend.The guard should be
<= 4(equivalently< 5), or the reads should be sequenced against a pre-computed end pointer.Reachability
json_parse_exis reached from:-c config.jsoninvocation viaread_jconf→json_parse_ex(all five binaries).ss-managerlocalhost UDP control channel, which accepts unauthenticated UDP commands (add,remove,ping) that embed a JSON payload. This is a pre-auth read of one heap byte on any host runningss-manager.The leaked byte is immediately fed to
hex_value()and compared to0xFF, so it is not directly a control-flow primitive — but it is still a real heap OOB read on untrusted input, and the JSON parser is a fork ofudp/json-parserwhich has had similar patches upstream that were never backported to this in-tree copy.Reproduction
Minimal reproducer (30 bytes) — restore from base64:
ASan output:
257 of 281 saved inputs from a 300 s libFuzzer round on
json_parse_excollapse to this root cause.Suggested fix
Change the guard to
<= 4:Or refactor to bulk-check then bulk-read:
The same off-by-one pattern also appears at
src/json.c:324-328(UTF-16 surrogate branch,< 6) and at lines 604 / 621 / 635 (literalstrue/false/null) — worth fixing as a family in one commit.Context
Found as part of an independent fuzzing audit of shadowsocks-libev v3.3.6.