-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Description
Hi, there is a potential bug in Json::Reader::parse where it stores dangling pointers to the string, read back during getFormattedErrorMessages.
This bug was reproduced on ca98c98.
Description
When invoking Reader::parse, it may store dangling pointers into the provided input string. These strings can be read back, e.g. by getFormattedErrorMessages which will cause a UAF if the original string is deallocated or goes out of scope.
Currently Reader::parse doesn't mention this dependency on the lifetime of the string in its documentation, so it may be a bit of a footgun for users.
Patch Suggestions
Option 1: copy string
If it's possible, you can copy either the whole input string or just the parts you need for error messages (rather than storing dangling pointers). For the latter choice, you only get additional runtime overhead in error cases (which might be acceptable).
Option 2: document
Alternatively, if option 1 is too cumbersome, you could simply document this dependency somewhere in Reader::parse, so its on end-users to enforce the in-scopeness of the string.
POC
The following testcase demonstrates the bug:
testcase.cpp
#include <string>
#include "/fuzz/install/include/json/json.h"
int main() {
Json::Reader reader; // legacy API
Json::Value root;
{
// Caller-owned buffer; pass raw pointers to Reader
std::string doc = "{\n \"a\": 1,\n trailing\n}"; // invalid JSON to force parse errors
const char* begin = doc.data();
const char* end = begin + doc.size();
(void)reader.parse(begin, end, root, /*collectComments=*/true);
// doc is destroyed here; Reader still keeps pointers into its buffer
}
// Later, Reader dereferences dangling pointers while formatting errors
Json::String errs = reader.getFormattedErrorMessages();
(void)errs;
return 0;
}
stdout
=================================================================
==1==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000000040 at pc 0x5568368f9963 bp 0x7fff6db80530 sp 0x7fff6db80528
READ of size 1 at 0x503000000040 thread T0
#0 0x5568368f9962 in Json::Reader::getLocationLineAndColumn[abi:cxx11](char const*) const (/fuzz/test+0x11d962) (BuildId: 0ad2958ed1bcc3bc4502175c00b5d61e3e4135c1)
#1 0x5568368f9cc8 in Json::Reader::getFormattedErrorMessages[abi:cxx11]() const (/fuzz/test+0x11dcc8) (BuildId: 0ad2958ed1bcc3bc4502175c00b5d61e3e4135c1)
#2 0x5568368e8be9 in main /fuzz/testcase.cpp:15:30
#3 0x7efcd1692d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#4 0x7efcd1692e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#5 0x55683680d844 in _start (/fuzz/test+0x31844) (BuildId: 0ad2958ed1bcc3bc4502175c00b5d61e3e4135c1)
0x503000000040 is located 0 bytes inside of 25-byte region [0x503000000040,0x503000000059)
freed by thread T0 here:
#0 0x5568368e6ebd in operator delete(void*) (/fuzz/test+0x10aebd) (BuildId: 0ad2958ed1bcc3bc4502175c00b5d61e3e4135c1)
#1 0x5568368e8bca in main /fuzz/testcase.cpp:13:3
#2 0x7efcd1692d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
previously allocated by thread T0 here:
#0 0x5568368e665d in operator new(unsigned long) (/fuzz/test+0x10a65d) (BuildId: 0ad2958ed1bcc3bc4502175c00b5d61e3e4135c1)
#1 0x5568368ea9e9 in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_construct<char const*>(char const*, char const*, std::forward_iterator_tag) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.tcc:219:14
#2 0x5568368e8ebc in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string<std::allocator<char>>(char const*, std::allocator<char> const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/basic_string.h:539:2
#3 0x5568368e8b50 in main /fuzz/testcase.cpp:8:23
#4 0x7efcd1692d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
SUMMARY: AddressSanitizer: heap-use-after-free (/fuzz/test+0x11d962) (BuildId: 0ad2958ed1bcc3bc4502175c00b5d61e3e4135c1) in Json::Reader::getLocationLineAndColumn[abi:cxx11](char const*) const
Shadow bytes around the buggy address:
0x502ffffffd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502ffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x503000000000: fa fa 00 00 00 fa fa fa[fd]fd fd fd fa fa fa fa
0x503000000080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==1==ABORTING
stderr
Steps to Reproduce
The crash was triaged with the following Dockerfile:
Dockerfile
# Ubuntu 22.04 with some packages pre-installed
FROM hgarrereyn/stitch_repro_base@sha256:3ae94cdb7bf2660f4941dc523fe48cd2555049f6fb7d17577f5efd32a40fdd2c
RUN git clone https://github.com/open-source-parsers/jsoncpp /fuzz/src && \
cd /fuzz/src && \
git checkout ca98c98457b1163cca1f7d8db62827c115fec6d1 && \
git submodule update --init --remote --recursive
ENV LD_LIBRARY_PATH=/fuzz/install/lib
ENV ASAN_OPTIONS=hard_rss_limit_mb=1024:detect_leaks=0
RUN echo '#!/bin/bash\nexec clang-17 -fsanitize=address -O0 "$@"' > /usr/local/bin/clang_wrapper && \
chmod +x /usr/local/bin/clang_wrapper && \
echo '#!/bin/bash\nexec clang++-17 -fsanitize=address -O0 "$@"' > /usr/local/bin/clang_wrapper++ && \
chmod +x /usr/local/bin/clang_wrapper++
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
cmake \
ninja-build \
&& rm -rf /var/lib/apt/lists/*
ENV CC=clang_wrapper \
CXX=clang_wrapper++
WORKDIR /fuzz/src
RUN cmake -S . -B build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/fuzz/install \
-DCMAKE_CXX_STANDARD=17 \
-DBUILD_SHARED_LIBS=OFF \
-DJSONCPP_WITH_TESTS=OFF \
-DJSONCPP_WITH_EXAMPLES=OFF \
-DJSONCPP_WITH_POST_BUILD_UNITTEST=OFF \
-DJSONCPP_WITH_CMAKE_PACKAGE=ON \
&& cmake --build build --target installBuild Command
clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -ljsoncpp && /fuzz/testReproduce
- Copy
Dockerfileandtestcase.cppinto a local folder. - Build the repro image:
docker build . -t repro --platform=linux/amd64- Compile and run the testcase in the image:
docker run \
-it --rm \
--platform linux/amd64 \
--mount type=bind,source="testcase.cpp",target=/fuzz/testcase.cpp \
repro \
bash -c "clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -ljsoncpp && /fuzz/test"Additional Info
This testcase was discovered by STITCH, an autonomous fuzzing system. All reports are reviewed manually (by a human) before submission.