Summary
Feeding a crafted input to ucl_add_string_fuzzer causes libucl to read uninitialized memory in the hash table implementation and then crash with a NULL pointer dereference in ucl_hash_destroy() while freeing the parser state.
Environment
- Tooling:
valgrind --tool=memcheck --track-origins=yes
- Target:
ucl_add_string_fuzzer
- OS: Ubuntu 20.04.6 LTS focal x86_64
- libucl version:
v0.9.2 (a6b5cac1121103984ee2035081b7467725d68ed7)
Reproducer
Artifacts:
- Fuzzer binary (
ucl_add_string_fuzzer)
- Single testcase
Repro with OSS-Fuzz helpers:
git clone https://github.com/google/oss-fuzz.git
cd oss-fuzz
python3 infra/helper.py build_image libucl
python3 infra/helper.py build_fuzzers --sanitizer=none libucl
python3 infra/helper.py shell libucl
apt update && apt install -y valgrind
ulimit -n 65535
valgrind --tool=memcheck --track-origins=yes /out/ucl_add_string_fuzzer /path/to/poc
Valgrind Trace (top frames):
==170== Use of uninitialised value of size 8
==170== at 0x215E8C: ucl_hash_destroy (in /out/ucl_add_string_fuzzer)
==170== by 0x2058E2: ucl_object_free_internal (in /out/ucl_add_string_fuzzer)
==170== by 0x2069BA: ucl_parser_free (in /out/ucl_add_string_fuzzer)
==170== by 0x200341: ucl_state_machine (in /out/ucl_add_string_fuzzer)
==170== by 0x1FCCE9: ucl_parser_add_chunk_full (in /out/ucl_add_string_fuzzer)
==170== Uninitialised value was created by a heap allocation at 0x483B723: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
UBSan Trace:
ERROR: SEGV on address 0x0 (READ)
#0 ucl_hash_destroy /src/libucl/src/ucl_hash.c:272:29
#1 ucl_object_free_internal /src/libucl/src/ucl_util.c:278
#2 ucl_parser_free /src/libucl/src/ucl_util.c:646
#3 ucl_state_machine /src/libucl/src/ucl_parser.c:2664
#4 ucl_parser_add_chunk_full /src/libucl/src/ucl_parser.c:3062
Likely root cause (from symptoms):
- During insertion,
kh_resize_ucl_hash_node() reallocs internal arrays but does not fully initialize new slots / flags, or error/partial-insert paths leave the table in an inconsistent state.
- On teardown,
ucl_hash_destroy() iterates the table using slot flags that contain uninitialized bytes, treats garbage as “occupied”, and accesses entry fields (or frees them), leading to a read from NULL (and potentially other invalid pointers).
Impact
- Denial of service via crash in consumers that parse untrusted UCL input.
- Depending on allocator state, teardown could attempt to free/walk garbage pointers (risking invalid free). No controlled memory corruption demonstrated here.
Additional testcase — same root cause via a different path
I’m attaching a second PoC (archive1.zip) that hits the same underlying defect (uninitialized slot/flag state leading to NULL/garbage deref in ucl_hash_destroy) but via a slightly different mutation path.
Credit: Aldo Ristori
archive0.zip
archive1.zip
Summary
Feeding a crafted input to
ucl_add_string_fuzzercauses libucl to read uninitialized memory in the hash table implementation and then crash with a NULL pointer dereference inucl_hash_destroy()while freeing the parser state.Environment
valgrind --tool=memcheck --track-origins=yesucl_add_string_fuzzerv0.9.2(a6b5cac1121103984ee2035081b7467725d68ed7)Reproducer
Artifacts:
ucl_add_string_fuzzer)Repro with OSS-Fuzz helpers:
Valgrind Trace (top frames):
UBSan Trace:
Likely root cause (from symptoms):
kh_resize_ucl_hash_node()reallocs internal arrays but does not fully initialize new slots / flags, or error/partial-insert paths leave the table in an inconsistent state.ucl_hash_destroy()iterates the table using slot flags that contain uninitialized bytes, treats garbage as “occupied”, and accesses entry fields (or frees them), leading to a read from NULL (and potentially other invalid pointers).Impact
Additional testcase — same root cause via a different path
I’m attaching a second PoC (
archive1.zip) that hits the same underlying defect (uninitialized slot/flag state leading to NULL/garbage deref inucl_hash_destroy) but via a slightly different mutation path.Credit: Aldo Ristori
archive0.zip
archive1.zip