From: Eric Biggers Date: Mon, 27 Mar 2023 00:25:46 +0000 (-0700) Subject: Improve fuzz testing X-Git-Tag: v1.14.0~63 X-Git-Url: https://wimlib.net/git/?a=commitdiff_plain;h=f73cef14c00125935485943482f3cd0c8b3c7ac6;p=wimlib Improve fuzz testing - Convert fuzzing scripts from afl-fuzz to libFuzzer - Add xml and wim fuzzers, including malloc failure injection - Fuzz for 2 minutes as part of the GitHub Actions CI --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4718b8f..29527bba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,3 +147,37 @@ jobs: mingw-w64-${{matrix.env}}-gcc pkgconf - run: CFLAGS="$DEF_CFLAGS" ./tools/make-windows-release --no-docs --no-zip + + fuzz-with-libFuzzer: + name: Fuzz with libFuzzer (${{matrix.target}} ${{matrix.sanitizer}}) + strategy: + matrix: + include: + - target: wim + sanitizer: + - target: wim + sanitizer: --asan --ubsan + - target: xml + sanitizer: + - target: xml + sanitizer: --asan --ubsan + - target: compress + sanitizer: + - target: compress + sanitizer: --asan --ubsan + - target: decompress + sanitizer: + - target: decompress + sanitizer: --asan --ubsan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang $DEPENDENCIES + - run: ./bootstrap + - name: Fuzz + run: | + tools/libFuzzer/fuzz.sh --time=120 ${{matrix.sanitizer}} \ + ${{matrix.target}} diff --git a/.gitignore b/.gitignore index feb1eaba..908421a5 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ /tests/tree-cmp /tests/wlfuzz /tests/wlfuzz.exe +/tools/libFuzzer/*/fuzz +/tools/libFuzzer/test-one-input /wimlib-*-bin/ /wimlib-*.tar /wimlib-*.tar.* diff --git a/tools/afl-fuzz/.gitignore b/tools/afl-fuzz/.gitignore deleted file mode 100644 index 24c8d6ce..00000000 --- a/tools/afl-fuzz/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*/fuzz diff --git a/tools/afl-fuzz/Makefile b/tools/afl-fuzz/Makefile deleted file mode 100644 index cff26bf6..00000000 --- a/tools/afl-fuzz/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -SRC := $(wildcard */*.c) -EXE := $(SRC:.c=) - -LDLIBS := -lwim -LDFLAGS := -L../../.libs -CPPFLAGS := -I../../include - -all:$(EXE) - -clean: - rm -f $(EXE) diff --git a/tools/afl-fuzz/compress/fuzz.c b/tools/afl-fuzz/compress/fuzz.c deleted file mode 100644 index 4c911725..00000000 --- a/tools/afl-fuzz/compress/fuzz.c +++ /dev/null @@ -1,63 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) -{ - int fd; - struct stat stbuf; - uint8_t ctype; - uint8_t level; - struct wimlib_compressor *c; - struct wimlib_decompressor *d; - size_t usize, csize; - void *udata, *cdata, *decompressed; - int ret; - - fd = open(argv[1], O_RDONLY); - assert(fd >= 0); - ret = fstat(fd, &stbuf); - assert(!ret); - - if (stbuf.st_size < 2) - return 0; - ret = read(fd, &ctype, 1); - assert(ret == 1); - ret = read(fd, &level, 1); - assert(ret == 1); - ctype = 1 + ((uint8_t)(ctype - 1) % 3); /* 1-3 */ - level = 1 + (level % 100); /* 1-100 */ - usize = stbuf.st_size - 2; - - udata = malloc(usize); - cdata = malloc(usize); - decompressed = malloc(usize); - - ret = read(fd, udata, usize); - assert(ret == usize); - - ret = wimlib_create_compressor(ctype, usize, level, &c); - if (ret == 0) { - ret = wimlib_create_decompressor(ctype, usize, &d); - assert(ret == 0); - - csize = wimlib_compress(udata, usize, cdata, usize, c); - if (csize) { - ret = wimlib_decompress(cdata, csize, - decompressed, usize, d); - assert(ret == 0); - assert(memcmp(udata, decompressed, usize) == 0); - } - wimlib_free_compressor(c); - wimlib_free_decompressor(d); - } - free(udata); - free(cdata); - free(decompressed); - return 0; -} diff --git a/tools/afl-fuzz/decompress/fuzz.c b/tools/afl-fuzz/decompress/fuzz.c deleted file mode 100644 index b3d4b90c..00000000 --- a/tools/afl-fuzz/decompress/fuzz.c +++ /dev/null @@ -1,46 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) -{ - int fd; - struct stat stbuf; - uint8_t ctype; - size_t csize, uspace; - void *cdata, *udata; - struct wimlib_decompressor *d; - int ret; - - fd = open(argv[1], O_RDONLY); - assert(fd >= 0); - ret = fstat(fd, &stbuf); - assert(!ret); - - if (stbuf.st_size < 1) - return 0; - ret = read(fd, &ctype, 1); - assert(ret == 1); - ctype = 1 + ((uint8_t)(ctype - 1) % 3); /* 1-3 */ - csize = stbuf.st_size - 1; - uspace = csize * 8; - - cdata = malloc(csize); - udata = malloc(uspace); - - ret = read(fd, cdata, csize); - assert(ret == csize); - - ret = wimlib_create_decompressor(ctype, uspace, &d); - if (ret == 0) - wimlib_decompress(cdata, csize, udata, uspace, d); - - free(udata); - free(cdata); - wimlib_free_decompressor(d); - return 0; -} diff --git a/tools/afl-fuzz/fuzz.sh b/tools/afl-fuzz/fuzz.sh deleted file mode 100755 index ea2bd4e4..00000000 --- a/tools/afl-fuzz/fuzz.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/bash - -set -e -u -o pipefail - -cd "$(dirname "$0")" - -read -r -a AVAILABLE_TARGETS < <(echo */fuzz.c | sed 's@/fuzz.c@@g') - -usage() -{ - cat << EOF -Usage: $0 [OPTION]... [TARGET]... - -Fuzz wimlib with afl-fuzz. - -Options: - --asan Enable AddressSanitizer - --no-resume Don't resume existing afl-fuzz session; start a new one - --ubsan Enable UndefinedBehaviorSanitizer - -Available targets: ${AVAILABLE_TARGETS[*]} -EOF -} - -die() -{ - echo "$*" 1>&2 - exit 1 -} - -asan=false -ubsan=false -may_resume=true - -longopts_array=( -asan -help -no-resume -ubsan -) -longopts=$(echo "${longopts_array[@]}" | tr ' ' ',') - -if ! options=$(getopt -o "" -l "$longopts" -- "$@"); then - usage 1>&2 - exit 1 -fi -eval set -- "$options" -while (( $# >= 0 )); do - case "$1" in - --asan) - asan=true - ;; - --help) - usage - exit 0 - ;; - --no-resume) - may_resume=false - ;; - --ubsan) - ubsan=true - ;; - --) - shift - break - ;; - *) - echo 1>&2 "Invalid option: \"$1\"" - usage 1>&2 - exit 1 - esac - shift -done - -if $asan && $ubsan; then - die "--asan and --ubsan are mutually exclusive" -fi - -if ! type -P afl-fuzz > /dev/null; then - die "afl-fuzz is not installed" -fi - -if (( $# == 0 )); then - targets=("${AVAILABLE_TARGETS[@]}") -else - for target; do - found=false - for t in "${AVAILABLE_TARGETS[@]}"; do - if [ "$target" = "$t" ]; then - found=true - fi - done - if ! $found; then - echo 1>&2 "Unknown target '$target'" - echo 1>&2 "Available targets: ${AVAILABLE_TARGETS[*]}" - exit 1 - fi - done - targets=("$@") -fi -if (( ${#targets[@]} > 1 )) && ! type -P urxvt > /dev/null; then - die "urxvt is not installed" -fi - -afl_opts="" -if $asan; then - export AFL_USE_ASAN=1 - export CFLAGS="-O2 -m32" - export CC=afl-clang - afl_opts+=" -m 800" -elif $ubsan; then - export CFLAGS="-fsanitize=undefined -fno-sanitize-recover=undefined" - export CC=afl-gcc -else - export AFL_HARDEN=1 - export CFLAGS="-O2" - export CC=afl-gcc -fi - -sudo sh -c "echo core > /proc/sys/kernel/core_pattern" -sudo sh -c "echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor" - -NPROC=$(getconf _NPROCESSORS_ONLN) - -( -cd ../../ -./configure CC="$CC" CFLAGS="$CFLAGS" -make "-j$NPROC" -) -make "-j$NPROC" -B -export LD_LIBRARY_PATH=$PWD/../../.libs - -for dir in "${targets[@]}"; do - workdir=/tmp/wimlib_$dir - cp -vaT "$dir" "$workdir" - indir=$workdir/inputs - outdir=$workdir/outputs - if [ -e "$outdir" ]; then - if $may_resume; then - indir="-" - else - rm -rf "${outdir:?}"/* - fi - else - mkdir "$outdir" - fi - cmd="afl-fuzz -i $indir -o $outdir -T wimlib_$dir $afl_opts -- $workdir/fuzz @@" - if (( ${#targets[@]} > 1 )); then - urxvt -e bash -c "$cmd" & - else - $cmd - fi -done -wait diff --git a/tools/afl-fuzz/compress/inputs/lzms20 b/tools/libFuzzer/compress/corpus/lzms20 similarity index 100% rename from tools/afl-fuzz/compress/inputs/lzms20 rename to tools/libFuzzer/compress/corpus/lzms20 diff --git a/tools/afl-fuzz/compress/inputs/lzms50 b/tools/libFuzzer/compress/corpus/lzms50 similarity index 100% rename from tools/afl-fuzz/compress/inputs/lzms50 rename to tools/libFuzzer/compress/corpus/lzms50 diff --git a/tools/afl-fuzz/compress/inputs/lzms80 b/tools/libFuzzer/compress/corpus/lzms80 similarity index 100% rename from tools/afl-fuzz/compress/inputs/lzms80 rename to tools/libFuzzer/compress/corpus/lzms80 diff --git a/tools/afl-fuzz/compress/inputs/lzx20 b/tools/libFuzzer/compress/corpus/lzx20 similarity index 100% rename from tools/afl-fuzz/compress/inputs/lzx20 rename to tools/libFuzzer/compress/corpus/lzx20 diff --git a/tools/afl-fuzz/compress/inputs/lzx50 b/tools/libFuzzer/compress/corpus/lzx50 similarity index 100% rename from tools/afl-fuzz/compress/inputs/lzx50 rename to tools/libFuzzer/compress/corpus/lzx50 diff --git a/tools/afl-fuzz/compress/inputs/lzx80 b/tools/libFuzzer/compress/corpus/lzx80 similarity index 100% rename from tools/afl-fuzz/compress/inputs/lzx80 rename to tools/libFuzzer/compress/corpus/lzx80 diff --git a/tools/afl-fuzz/compress/inputs/xpress20 b/tools/libFuzzer/compress/corpus/xpress20 similarity index 100% rename from tools/afl-fuzz/compress/inputs/xpress20 rename to tools/libFuzzer/compress/corpus/xpress20 diff --git a/tools/afl-fuzz/compress/inputs/xpress50 b/tools/libFuzzer/compress/corpus/xpress50 similarity index 100% rename from tools/afl-fuzz/compress/inputs/xpress50 rename to tools/libFuzzer/compress/corpus/xpress50 diff --git a/tools/afl-fuzz/compress/inputs/xpress80 b/tools/libFuzzer/compress/corpus/xpress80 similarity index 100% rename from tools/afl-fuzz/compress/inputs/xpress80 rename to tools/libFuzzer/compress/corpus/xpress80 diff --git a/tools/libFuzzer/compress/fuzz.c b/tools/libFuzzer/compress/fuzz.c new file mode 100644 index 00000000..e87bbba9 --- /dev/null +++ b/tools/libFuzzer/compress/fuzz.c @@ -0,0 +1,44 @@ +#include "../fuzzer.h" + +/* Fuzz the compression and decompression round trip. */ +int LLVMFuzzerTestOneInput(const uint8_t *in, size_t insize) +{ + int ctype; + int level; + struct wimlib_compressor *c; + struct wimlib_decompressor *d; + size_t csize_avail = insize; + uint8_t *cbuf; + uint8_t *decompressed; + size_t csize; + int ret; + + if (insize < 2) + return 0; + ctype = 1 + ((uint8_t)(in[0] - 1) % 3); /* 1-3 */ + level = 1 + (in[1] % 100); /* 1-100 */ + in += 2; + insize -= 2; + + cbuf = malloc(csize_avail); + decompressed = malloc(insize); + + ret = wimlib_create_compressor(ctype, insize, level, &c); + if (ret == 0) { + ret = wimlib_create_decompressor(ctype, insize, &d); + assert(ret == 0); + + csize = wimlib_compress(in, insize, cbuf, csize_avail, c); + if (csize) { + ret = wimlib_decompress(cbuf, csize, + decompressed, insize, d); + assert(ret == 0); + assert(memcmp(in, decompressed, insize) == 0); + } + wimlib_free_compressor(c); + wimlib_free_decompressor(d); + } + free(cbuf); + free(decompressed); + return 0; +} diff --git a/tools/afl-fuzz/decompress/inputs/lzms b/tools/libFuzzer/decompress/corpus/lzms similarity index 100% rename from tools/afl-fuzz/decompress/inputs/lzms rename to tools/libFuzzer/decompress/corpus/lzms diff --git a/tools/afl-fuzz/decompress/inputs/lzx b/tools/libFuzzer/decompress/corpus/lzx similarity index 100% rename from tools/afl-fuzz/decompress/inputs/lzx rename to tools/libFuzzer/decompress/corpus/lzx diff --git a/tools/afl-fuzz/decompress/inputs/xpress b/tools/libFuzzer/decompress/corpus/xpress similarity index 100% rename from tools/afl-fuzz/decompress/inputs/xpress rename to tools/libFuzzer/decompress/corpus/xpress diff --git a/tools/libFuzzer/decompress/fuzz.c b/tools/libFuzzer/decompress/fuzz.c new file mode 100644 index 00000000..82782bc3 --- /dev/null +++ b/tools/libFuzzer/decompress/fuzz.c @@ -0,0 +1,26 @@ +#include "../fuzzer.h" + +/* Fuzz decompression. */ +int LLVMFuzzerTestOneInput(const uint8_t *in, size_t insize) +{ + int ctype; + struct wimlib_decompressor *d; + const size_t outsize_avail = 3 * insize; + uint8_t *out; + int ret; + + if (insize < 1) + return 0; + ctype = 1 + ((uint8_t)(in[0] - 1) % 3); /* 1-3 */ + in++; + insize--; + + ret = wimlib_create_decompressor(ctype, insize, &d); + if (ret == 0) { + out = malloc(outsize_avail); + wimlib_decompress(in, insize, out, outsize_avail, d); + wimlib_free_decompressor(d); + free(out); + } + return 0; +} diff --git a/tools/libFuzzer/fault-injection.c b/tools/libFuzzer/fault-injection.c new file mode 100644 index 00000000..e33c37a6 --- /dev/null +++ b/tools/libFuzzer/fault-injection.c @@ -0,0 +1,42 @@ +#include "fuzzer.h" + +static int64_t num_allocs_remaining; + +static void * +faultinject_malloc(size_t size) +{ + if (__atomic_sub_fetch(&num_allocs_remaining, 1, __ATOMIC_RELAXED) <= 0) + return NULL; + return malloc(size); +} + +static void +faultinject_free(void *p) +{ + free(p); +} + +static void * +faultinject_realloc(void *p, size_t size) +{ + if (__atomic_sub_fetch(&num_allocs_remaining, 1, __ATOMIC_RELAXED) <= 0) + return NULL; + return realloc(p, size); +} + +bool +setup_fault_nth(const uint8_t **in, size_t *insize, uint16_t *fault_nth) +{ + uint16_t n; + + if (*insize < 2) + return false; + memcpy(&n, *in, 2); + wimlib_set_memory_allocator(faultinject_malloc, faultinject_free, + faultinject_realloc); + num_allocs_remaining = n ?: INT64_MAX; + *in += 2; + *insize -= 2; + *fault_nth = n; + return true; +} diff --git a/tools/libFuzzer/fuzz.sh b/tools/libFuzzer/fuzz.sh new file mode 100755 index 00000000..247587d6 --- /dev/null +++ b/tools/libFuzzer/fuzz.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +set -e -u -o pipefail + +cd "$(dirname "$0")" +TOPDIR=../.. +SCRIPTDIR=$PWD + +read -r -a AVAILABLE_TARGETS < <(echo */fuzz.c | sed 's@/fuzz.c@@g') + +usage() +{ + cat << EOF +Usage: $0 [OPTION]... FUZZ_TARGET + +Fuzz wimlib with LLVM's libFuzzer. + +Options: + --asan Enable AddressSanitizer + --input=INPUT Test a single input file only + --msan Enable MemorySanitizer + --time=SECONDS Stop after the given time has passed + --ubsan Enable UndefinedBehaviorSanitizer + +Available fuzz targets: ${AVAILABLE_TARGETS[*]} +EOF +} + +die() +{ + echo "$*" 1>&2 + exit 1 +} + +run_cmd() +{ + echo "$*" + "$@" +} + +EXTRA_SANITIZERS= +EXTRA_FUZZER_ARGS=() +INPUT= + +longopts_array=( +asan +help +input: +msan +time: +ubsan +) +longopts=$(echo "${longopts_array[@]}" | tr ' ' ',') + +if ! options=$(getopt -o "" -l "$longopts" -- "$@"); then + usage 1>&2 + exit 1 +fi +eval set -- "$options" +while true; do + case "$1" in + --asan) + EXTRA_SANITIZERS+=",address" + ;; + --help) + usage + exit 0 + ;; + --input) + INPUT=$2 + shift + ;; + --time) + EXTRA_FUZZER_ARGS+=("-max_total_time=$2") + shift + ;; + --msan) + EXTRA_SANITIZERS+=",memory" + ;; + --ubsan) + EXTRA_SANITIZERS+=",undefined" + ;; + --) + shift + break + ;; + *) + echo 1>&2 "Invalid option '$1'" + usage 1>&2 + exit 1 + esac + shift +done + +if (( $# != 1 )); then + echo 1>&2 "No fuzz target specified!" + usage 1>&2 + exit 1 +fi +TARGET=$1 +if [ ! -e "$TARGET/fuzz.c" ]; then + echo 1>&2 "'$TARGET' is not a valid fuzz target!" + usage 1>&2 + exit 1 +fi +cd "$TOPDIR" +cflags="-g -O1 -Wall -Werror" +cflags+=" -fsanitize=fuzzer-no-link$EXTRA_SANITIZERS" +if [ -n "$EXTRA_SANITIZERS" ]; then + cflags+=" -fno-sanitize-recover=${EXTRA_SANITIZERS#,}" +fi +if ! [ -e config.log ] || ! grep -q -- "'CFLAGS=$cflags'" config.log; then + run_cmd ./configure --enable-test-support --without-fuse --without-ntfs-3g \ + CC=clang CFLAGS="$cflags" +fi +run_cmd make "-j$(getconf _NPROCESSORS_ONLN)" +cd "$SCRIPTDIR" +if [ -n "$INPUT" ]; then + run_cmd clang -g -O1 -fsanitize=fuzzer-no-link$EXTRA_SANITIZERS -Wall -Werror \ + -I "$TOPDIR/include" "$TARGET/fuzz.c" test-one-input.c fault-injection.c \ + "$TOPDIR/.libs/libwim.a" -o test-one-input + run_cmd ./test-one-input "$INPUT" +else + run_cmd clang -g -O1 -fsanitize=fuzzer$EXTRA_SANITIZERS -Wall -Werror \ + -I "$TOPDIR/include" "$TARGET/fuzz.c" fault-injection.c \ + "$TOPDIR/.libs/libwim.a" -o "$TARGET/fuzz" + run_cmd "$TARGET/fuzz" "${EXTRA_FUZZER_ARGS[@]}" "$TARGET/corpus" +fi diff --git a/tools/libFuzzer/fuzzer.h b/tools/libFuzzer/fuzzer.h new file mode 100644 index 00000000..6639060c --- /dev/null +++ b/tools/libFuzzer/fuzzer.h @@ -0,0 +1,13 @@ +#define ENABLE_TEST_SUPPORT 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool +setup_fault_nth(const uint8_t **in, size_t *insize, uint16_t *fault_nth); diff --git a/tools/libFuzzer/test-one-input.c b/tools/libFuzzer/test-one-input.c new file mode 100644 index 00000000..9b201f86 --- /dev/null +++ b/tools/libFuzzer/test-one-input.c @@ -0,0 +1,34 @@ +#include +#include +#include +#include +#include +#include + +int LLVMFuzzerTestOneInput(const uint8_t *in, size_t insize); + +int main(int argc, char *argv[]) +{ + int fd; + struct stat stbuf; + uint8_t *in; + + fd = open(argv[1], O_RDONLY); + if (fd < 0) { + perror(argv[1]); + return 1; + } + if (fstat(fd, &stbuf) != 0) { + perror("fstat"); + return 1; + } + in = malloc(stbuf.st_size); + if (read(fd, in, stbuf.st_size) != stbuf.st_size) { + perror("read"); + return 1; + } + LLVMFuzzerTestOneInput(in, stbuf.st_size); + close(fd); + free(in); + return 0; +} diff --git a/tools/libFuzzer/wim/corpus/0 b/tools/libFuzzer/wim/corpus/0 new file mode 100644 index 00000000..a2a3f959 Binary files /dev/null and b/tools/libFuzzer/wim/corpus/0 differ diff --git a/tools/libFuzzer/wim/fuzz.c b/tools/libFuzzer/wim/fuzz.c new file mode 100644 index 00000000..4d21e12b --- /dev/null +++ b/tools/libFuzzer/wim/fuzz.c @@ -0,0 +1,32 @@ +#include "../fuzzer.h" + +/* Fuzz WIM file reading. */ +int LLVMFuzzerTestOneInput(const uint8_t *in, size_t insize) +{ + uint16_t fault_nth; + char tmp_wim[128]; + char tmp_dir[128]; + int fd; + WIMStruct *wim; + int ret; + + if (!setup_fault_nth(&in, &insize, &fault_nth)) + return 0; + + sprintf(tmp_wim, "/tmp/wim-fuzz-%d.wim", getpid()); + sprintf(tmp_dir, "/tmp/wim-fuzz-%d", getpid()); + + fd = open(tmp_wim, O_WRONLY|O_CREAT|O_TRUNC, 0600); + assert(fd >= 0); + ret = write(fd, in, insize); + assert(ret == insize); + close(fd); + + ret = wimlib_open_wim(tmp_wim, 0, &wim); + if (ret == 0) { + wimlib_extract_image(wim, 1, tmp_dir, 0); + wimlib_add_image(wim, tmp_dir, "name", NULL, 0); + wimlib_free(wim); + } + return 0; +} diff --git a/tools/libFuzzer/xml/corpus/0 b/tools/libFuzzer/xml/corpus/0 new file mode 100644 index 00000000..d63b464a Binary files /dev/null and b/tools/libFuzzer/xml/corpus/0 differ diff --git a/tools/libFuzzer/xml/fuzz.c b/tools/libFuzzer/xml/fuzz.c new file mode 100644 index 00000000..a16dbe96 --- /dev/null +++ b/tools/libFuzzer/xml/fuzz.c @@ -0,0 +1,38 @@ +#include "../fuzzer.h" + +/* Fuzz XML parsing and writing. */ +int LLVMFuzzerTestOneInput(const uint8_t *in, size_t insize) +{ + uint16_t fault_nth; + char *in_str; + char *out_str = NULL; + int ret; + + if (!setup_fault_nth(&in, &insize, &fault_nth)) + return 0; + + in_str = malloc(insize + 1); + memcpy(in_str, in, insize); + in_str[insize] = '\0'; + ret = wimlib_parse_and_write_xml_doc(in_str, &out_str); + if (ret == 0) { + char *out2_str = NULL; + + /* + * If the first parse+write succeeded, we now should be able to + * parse+write the result without changing it further. + */ + ret = wimlib_parse_and_write_xml_doc(out_str, &out2_str); + if (ret != 0) + assert(ret == WIMLIB_ERR_NOMEM && fault_nth); + else + assert(strcmp(out_str, out2_str) == 0); + free(out2_str); + } else { + assert(ret == WIMLIB_ERR_XML || + (fault_nth && ret == WIMLIB_ERR_NOMEM)); + } + free(in_str); + free(out_str); + return 0; +}