Name and Version
Binary is the brotli CLI tool built from the official Google Brotli repository.
Version 1.2.0. Git commit 037b70e.
Built with CMake and Clang using ASan and UBSan instrumentation to make the crash observable. A normal release build has the same bug, it just may not print sanitizer diagnostics before crashing.
git clone https://github.com/google/brotli.git
cd brotli
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-fsanitize=address,undefined -g -O0" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build build -j4
The resulting binary is at build/brotli.
Operating systems
Linux. Confirmed on Ubuntu 24.04.2 LTS x86_64. The vulnerable code has no OS specific logic, so any platform that can build the CLI tool with a normal C stack is expected to be affected, including macOS and Windows builds.
Which modules do you know to be affected?
c/tools/brotli.c, function ParseParams.
Command line
brotli invoked with 500 empty string arguments, built with the bash loop shown in the reproduction steps below since typing 500 literal empty arguments by hand is not practical.
The array has 24 slots, so the language level bound is crossed on the 25th empty string argument. The exact count needed for a crash that AddressSanitizer actually reports depends on compiler and stack layout. On the tested build, 25 to 33 empty arguments only trip a silent UBSan warning without aborting, and 34 or more reliably produces the AddressSanitizer report and abort shown below. This report uses 500 to remove any doubt, and that exact count was independently re-run and confirmed before writing this report.
Problem description & steps to reproduce
ParseParams in c/tools/brotli.c parses argv in a loop and records the index of every "not an input file" argument into context.not_input_indices, a fixed size array declared as int not_input_indices[MAX_OPTIONS] with MAX_OPTIONS equal to 24. The intended bounds check is:
if (next_option_index > (MAX_OPTIONS - 2)) {
fprintf(stderr, "too many options passed\n");
return COMMAND_INVALID;
}
This check only sits on the code path for normal length arguments. For an empty string argument, arg_len == 0, the loop takes an earlier branch at line 304 that writes to the array and calls continue before the bounds check is ever reached.
if (arg_len == 0) {
params->not_input_indices[next_option_index++] = i;
continue;
}
Since the array holds 24 elements, the 25th empty string argument makes next_option_index reach 24 and write one element past the end of the array. context is a local variable on the stack of main, so this is a stack buffer overflow with an attacker influenced, monotonically increasing 32 bit value. Each additional empty string argument keeps writing further past the array. On the tested build, AddressSanitizer only reports the overflow once the write reaches roughly the 34th empty string argument, since the writes before that land in stack padding next to the array. This report uses 500 empty string arguments so the crash is unambiguous regardless of compiler or stack layout.
Steps to reproduce.
Build the project with ASan and UBSan as shown above. No model file, dictionary file, or other input file is needed.
Run the binary with 500 empty string arguments. This uses bash array expansion so no manual typing of hundreds of arguments is required.
BIN=./build/brotli
args=()
for ((k=0; k<500; k++)); do args+=(""); done
"$BIN" "${args[@]}"
The process crashes immediately during argument parsing, before any file is opened or any compression happens.
First Bad Commit
Not determined. The vulnerable bounds check placement in ParseParams appears to be present since the CLI tool's not_input_indices mechanism was introduced and is still present in the current main branch at commit 037b70e.
Relevant log output
/path/to/brotli/c/tools/brotli.c:305:32: runtime error: index 24 out of bounds for type 'int [24]'
=================================================================
==1404993==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x71ec9cc09218 at pc 0x59ae061f9b08 bp 0x7ffdccf81d10 sp 0x7ffdccf81d00
WRITE of size 4 at 0x71ec9cc09218 thread T0
#0 0x59ae061f9b07 in ParseParams /path/to/brotli/c/tools/brotli.c:305
#1 0x59ae06209eb2 in main /path/to/brotli/c/tools/brotli.c:1522
#2 0x71ec9ee2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x71ec9ee2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x59ae061f8964 in _start (/path/to/brotli/build/brotli+0x1c964)
Address 0x71ec9cc09218 is located in stack of thread T0 at offset 536 in frame
#0 0x59ae06209499 in main /path/to/brotli/c/tools/brotli.c:1480
This frame has 1 object(s):
[48, 536) 'context' (line 1482) <== Memory access at offset 536 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow /path/to/brotli/c/tools/brotli.c:305 in ParseParams
==1404993==ABORTING
The crash was reproduced three separate times with 500, 5000, and 20000 empty string arguments, all with the identical stack trace, identical overflowed variable context, and identical exit behavior.
Impact
An attacker who controls the argv array passed to the brotli CLI tool, even without controlling any file content, can corrupt the stack frame of main. Context context holds pointers used later in the program, such as argv, dictionary, decoder, and file handles, so continued out of bounds writes past the array can overwrite these pointers with attacker chosen sequential integers. This gives a memory corruption primitive reachable purely through argument construction.
This is realistically triggerable whenever brotli is invoked by another program that builds the argument list programmatically, for example a wrapper script, a build system, or a service that shells out to brotli with a file list where some path or field can become an empty string. In the simplest case the result is a crash and denial of service. Depending on stack layout, compiler, and mitigations in the specific build, further corruption of nearby pointers or the stack canary could raise the impact toward memory corruption beyond a controlled crash.
Fix
Move the bounds check so it runs before every write to not_input_indices, not only on the non-empty argument path. The simplest fix is to place the existing check at the top of the loop body, before the arg_len == 0 branch, so both write sites share the same guard.
for (i = 1; i < argc; ++i) {
const char* arg = argv[i];
size_t arg_len = arg ? strlen(arg) : 0;
if (next_option_index > (MAX_OPTIONS - 2)) {
fprintf(stderr, "too many options passed\n");
return COMMAND_INVALID;
}
if (arg_len == 0) {
params->not_input_indices[next_option_index++] = i;
continue;
}
...
Name and Version
Binary is the
brotliCLI tool built from the official Google Brotli repository.Version 1.2.0. Git commit 037b70e.
Built with CMake and Clang using ASan and UBSan instrumentation to make the crash observable. A normal release build has the same bug, it just may not print sanitizer diagnostics before crashing.
The resulting binary is at
build/brotli.Operating systems
Linux. Confirmed on Ubuntu 24.04.2 LTS x86_64. The vulnerable code has no OS specific logic, so any platform that can build the CLI tool with a normal C stack is expected to be affected, including macOS and Windows builds.
Which modules do you know to be affected?
c/tools/brotli.c, functionParseParams.Command line
brotli invoked with 500 empty string arguments, built with the bash loop shown in the reproduction steps below since typing 500 literal empty arguments by hand is not practical.
The array has 24 slots, so the language level bound is crossed on the 25th empty string argument. The exact count needed for a crash that AddressSanitizer actually reports depends on compiler and stack layout. On the tested build, 25 to 33 empty arguments only trip a silent UBSan warning without aborting, and 34 or more reliably produces the AddressSanitizer report and abort shown below. This report uses 500 to remove any doubt, and that exact count was independently re-run and confirmed before writing this report.
Problem description & steps to reproduce
ParseParamsinc/tools/brotli.cparsesargvin a loop and records the index of every "not an input file" argument intocontext.not_input_indices, a fixed size array declared asint not_input_indices[MAX_OPTIONS]withMAX_OPTIONSequal to 24. The intended bounds check is:This check only sits on the code path for normal length arguments. For an empty string argument,
arg_len == 0, the loop takes an earlier branch at line 304 that writes to the array and callscontinuebefore the bounds check is ever reached.Since the array holds 24 elements, the 25th empty string argument makes
next_option_indexreach 24 and write one element past the end of the array.contextis a local variable on the stack ofmain, so this is a stack buffer overflow with an attacker influenced, monotonically increasing 32 bit value. Each additional empty string argument keeps writing further past the array. On the tested build, AddressSanitizer only reports the overflow once the write reaches roughly the 34th empty string argument, since the writes before that land in stack padding next to the array. This report uses 500 empty string arguments so the crash is unambiguous regardless of compiler or stack layout.Steps to reproduce.
Build the project with ASan and UBSan as shown above. No model file, dictionary file, or other input file is needed.
Run the binary with 500 empty string arguments. This uses bash array expansion so no manual typing of hundreds of arguments is required.
The process crashes immediately during argument parsing, before any file is opened or any compression happens.
First Bad Commit
Not determined. The vulnerable bounds check placement in
ParseParamsappears to be present since the CLI tool'snot_input_indicesmechanism was introduced and is still present in the current main branch at commit 037b70e.Relevant log output
The crash was reproduced three separate times with 500, 5000, and 20000 empty string arguments, all with the identical stack trace, identical overflowed variable
context, and identical exit behavior.Impact
An attacker who controls the argv array passed to the
brotliCLI tool, even without controlling any file content, can corrupt the stack frame ofmain.Context contextholds pointers used later in the program, such asargv,dictionary,decoder, and file handles, so continued out of bounds writes past the array can overwrite these pointers with attacker chosen sequential integers. This gives a memory corruption primitive reachable purely through argument construction.This is realistically triggerable whenever
brotliis invoked by another program that builds the argument list programmatically, for example a wrapper script, a build system, or a service that shells out tobrotliwith a file list where some path or field can become an empty string. In the simplest case the result is a crash and denial of service. Depending on stack layout, compiler, and mitigations in the specific build, further corruption of nearby pointers or the stack canary could raise the impact toward memory corruption beyond a controlled crash.Fix
Move the bounds check so it runs before every write to
not_input_indices, not only on the non-empty argument path. The simplest fix is to place the existing check at the top of the loop body, before thearg_len == 0branch, so both write sites share the same guard.