Summary
When config_read_string() is given malformed input that the flex-generated scanner cannot recover from, libconfig terminates the entire calling process via yy_fatal_error() → exit(YY_EXIT_FAILURE). The library should return an error code instead so the application can handle it.
This was previously reported in #56 (2016), where the failure was attributed to "an I/O error on the file or stream you're trying to read". The 12-byte string-only reproducer below shows the failure has nothing to do with I/O — it is a parser-state failure on adversarial input.
Environment
- libconfig:
v1.8.2 (latest stable)
- Compiler: clang 22.1.6 (Homebrew LLVM)
- Platform: macOS 14 / Apple Silicon
- Build:
cmake -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF with -fsanitize=fuzzer-no-link,address
Minimal reproducer (12 bytes)
#include <libconfig.h>
int main(void) {
config_t cfg;
config_init(&cfg);
config_read_string(&cfg, "@include \".\""); /* 12 bytes */
/* never reached */
config_destroy(&cfg);
return 0;
}
Build & run:
$ clang repro.c -lconfig -o repro && ./repro
input in flex scanner failed
$ echo $?
1
The process is terminated before config_read_string() returns. The caller has no way to detect or recover from this — config_init() succeeded, the input is a pure in-memory string (no FILE*, no fd, no path), and yet the process is gone.
Stack trace (under ASan)
SUMMARY: libFuzzer: fuzz target exited
#5 yy_fatal_error scanner.c:2463
#6 yy_get_next_buffer scanner.c:1903
#7 libconfig_yylex scanner.c:1742
#8 libconfig_yyparse grammar.c:1231
#9 __config_read libconfig.c:582
Impact
Any application that feeds libconfig untrusted (or even partially attacker-influenced) configuration data — for example, services that accept user-supplied config snippets, IPC handlers, file watchers — can be silently DoS'd by a 12-byte input. The application gets no opportunity to log the failure or fall back; the exit() is unconditional.
Suggested fix
yy_fatal_error() is auto-generated by flex but its body can be overridden with %option yyfatalerror=... or by defining YY_FATAL_ERROR(msg) in the scanner prologue. A library-grade behavior would set a state flag, longjmp back to the parse-entry frame, or store the message in config_error_text / config_error_line so the existing config_read* API can return CONFIG_FALSE instead of crashing.
Discovered by 12-hour libFuzzer (fork mode) coverage-guided fuzzing of config_read_string. After dedup, 226 distinct fuzzer-saved crashes all reduced to this one root cause.
Summary
When
config_read_string()is given malformed input that the flex-generated scanner cannot recover from, libconfig terminates the entire calling process viayy_fatal_error()→exit(YY_EXIT_FAILURE). The library should return an error code instead so the application can handle it.This was previously reported in #56 (2016), where the failure was attributed to "an I/O error on the file or stream you're trying to read". The 12-byte string-only reproducer below shows the failure has nothing to do with I/O — it is a parser-state failure on adversarial input.
Environment
v1.8.2(latest stable)cmake -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFFwith-fsanitize=fuzzer-no-link,addressMinimal reproducer (12 bytes)
Build & run:
The process is terminated before
config_read_string()returns. The caller has no way to detect or recover from this —config_init()succeeded, the input is a pure in-memory string (noFILE*, no fd, no path), and yet the process is gone.Stack trace (under ASan)
Impact
Any application that feeds libconfig untrusted (or even partially attacker-influenced) configuration data — for example, services that accept user-supplied config snippets, IPC handlers, file watchers — can be silently DoS'd by a 12-byte input. The application gets no opportunity to log the failure or fall back; the
exit()is unconditional.Suggested fix
yy_fatal_error()is auto-generated by flex but its body can be overridden with%option yyfatalerror=...or by definingYY_FATAL_ERROR(msg)in the scanner prologue. A library-grade behavior would set a state flag, longjmp back to the parse-entry frame, or store the message inconfig_error_text/config_error_lineso the existingconfig_read*API can returnCONFIG_FALSEinstead of crashing.Discovered by 12-hour libFuzzer (fork mode) coverage-guided fuzzing of
config_read_string. After dedup, 226 distinct fuzzer-saved crashes all reduced to this one root cause.