Skip to content

heap-overflow (write) via tidyLoadConfig #1169

@hgarrereyn

Description

@hgarrereyn

Hi, there is a potential bug in prvTidyDefinePriorityAttribute reachable by providing a crafted config file to tidyLoadConfig.

This bug was reproduced on d08ddc2.

Description

The fuzzer calls tidyCreate() followed by tidyLoadConfig() with a generated configuration file. The heap-buffer-overflow is reproduced with a minimal config containing 10 space-separated items for the priority-attributes option. In attrs.c:prvTidyDefinePriorityAttribute, the code appends a new string to priorities->list at index priorities->count, increments count, and then writes a terminating NULL at priorities->list[priorities->count]. When the internal list capacity is exactly 10 pointers (80 bytes as shown by ASan), adding the 10th item fills the buffer, and writing the terminating NULL occurs at index 10, which is one past the allocated array, causing a heap OOB write.

Specifically the issue seems to be this logic:

tidy-html5/src/attrs.c

Lines 976 to 984 in d08ddc2

if ( priorities->count >= priorities->capacity )
{
priorities->capacity = priorities->capacity * 2;
priorities->list = TidyRealloc(doc->allocator, priorities->list, sizeof(tmbstr) * priorities->capacity + 1 );
}
priorities->list[priorities->count] = TY_(tmbstrdup)( doc->allocator, name);
priorities->count++;
priorities->list[priorities->count] = NULL;

When priorities->count == priorities->capacity, the count will be incremented beyond the capacity and a null value is written.

Note that this testcase would in theory also be reachable via the oss-fuzz tidy_config_fuzzer.

POC

The following testcase demonstrates the bug:

testcase.cpp

#include <cstdio>
#include <cstdlib>
#include <cstring>
extern "C" {
#include "/fuzz/install/include/tidy.h"
}
int main(){
    TidyDoc doc = tidyCreate();
    if(!doc) return 0;
    const char* cfg_path = "/tmp/tidy_cfg_min.txt";
    FILE* f = fopen(cfg_path, "w");
    if(!f) return 0;
    // Exactly 10 items trigger the overflow by exhausting the 10-pointer buffer
    const char* cfg =
        "priority-attributes: img:alt(2) table:summary(3) a:href(2) link:href(2) meta:content(1) input:name(1) script:type(1) img:src(2) div:id(1) span:class(1)\n";
    fwrite(cfg, 1, strlen(cfg), f);
    fclose(f);
    tidyLoadConfig(doc, cfg_path);
    return 0;
}

stdout

=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x507000000070 at pc 0x56288698da19 bp 0x7fff382726d0 sp 0x7fff382726c8
WRITE of size 8 at 0x507000000070 thread T0
    #0 0x56288698da18 in prvTidyDefinePriorityAttribute (/fuzz/test+0x1a1a18) (BuildId: 33b805a0c6d047cc5e7a3a3f7d32572f792bc34d)
    #1 0x562886a05ccb in prvTidyDeclareListItem (/fuzz/test+0x219ccb) (BuildId: 33b805a0c6d047cc5e7a3a3f7d32572f792bc34d)
    #2 0x562886a01a5b in ParseList config.c
    #3 0x562886a03dfc in prvTidyParseConfigFileEnc (/fuzz/test+0x217dfc) (BuildId: 33b805a0c6d047cc5e7a3a3f7d32572f792bc34d)
    #4 0x56288697f549 in main /fuzz/testcase.cpp:18:5
    #5 0x7f398f2f0d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #6 0x7f398f2f0e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #7 0x5628868a4394 in _start (/fuzz/test+0xb8394) (BuildId: 33b805a0c6d047cc5e7a3a3f7d32572f792bc34d)

0x507000000070 is located 0 bytes after 80-byte region [0x507000000020,0x507000000070)
allocated by thread T0 here:
    #0 0x56288694073e in malloc (/fuzz/test+0x15473e) (BuildId: 33b805a0c6d047cc5e7a3a3f7d32572f792bc34d)
    #1 0x562886a0a4c3 in defaultAlloc alloc.c

SUMMARY: AddressSanitizer: heap-buffer-overflow (/fuzz/test+0x1a1a18) (BuildId: 33b805a0c6d047cc5e7a3a3f7d32572f792bc34d) in prvTidyDefinePriorityAttribute
Shadow bytes around the buggy address:
  0x506ffffffd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x506ffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x506ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x506fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x506fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x507000000000: fa fa fa fa 00 00 00 00 00 00 00 00 00 00[fa]fa
  0x507000000080: fa fa fd fd fd fd fd fd fd fd fd fa fa fa fa fa
  0x507000000100: fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa
  0x507000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x507000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x507000000280: 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/htacg/tidy-html5.git /fuzz/src && \
    cd /fuzz/src && \
    git checkout d08ddc2860aa95ba8e301343a30837f157977cba && \
    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
RUN apt-get update && apt-get install -y --no-install-recommends \
    cmake \
    ninja-build \
 && rm -rf /var/lib/apt/lists/*

# Configure and build libtidy (static only) and install to /fuzz/install
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 \
    -DBUILD_SHARED_LIB=OFF \
    -DSUPPORT_CONSOLE_APP=OFF \
    -DBUILD_SAMPLE_CODE=OFF \
    -DBUILD_TAB2SPACE=OFF

RUN cmake --build build --target install --config Release

Build Command

clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -ltidy && /fuzz/test

Reproduce

  1. Copy Dockerfile and testcase.cpp into a local folder.
  2. Build the repro image:
docker build . -t repro --platform=linux/amd64
  1. 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 -ltidy && /fuzz/test"


Additional Info

This testcase was discovered by STITCH, an autonomous fuzzing system. All reports are reviewed manually (by a human) before submission.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions