# Bug Report

## Describe the bug  
Calling file_printf(ms, fmt) with a heap format string that ends in an incomplete specifier (e.g., "%") triggers a heap-buffer-over-read in libmagic’s file_checkfmt() (funcs.c). The parser walks past the terminating NUL and dereferences one byte beyond the allocated buffer while scanning flags/length modifiers.
ASan shows:
```
==1938837==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50b0000000a5
READ of size 1 at 0x50b0000000a5 thread T0
#0 file_checkfmt  funcs.c:103
#1 file_vprintf   funcs.c:142
#2 file_printf    funcs.c:182
...
0x50b0000000a5 is 0 bytes after 101-byte region [0x50b000000040,0x50b0000000a5)
```
That is, fmt was allocated as fmt_len+1 == 101 bytes; the read happens at fmt + fmt_len + 1 (one past the NUL).

# Root Cause Analysis
- file_checkfmt() parses fmt with a pointer p and repeatedly dereferences *p inside loops like:
```
while (strchr("#0.'+- ", *p) != NULL) p++;      // funcs.c:103
if (*p == '*') p++;
while (isdigit((unsigned char)*p)) p++;
if (*p == '.') { p++; while (isdigit((unsigned char)*p)) p++; }
while (strchr("hljztL", *p) != NULL) p++;
// switch(*p) ...
```
- When fmt ends with '%' or otherwise forms an incomplete format, p can advance to point at the NUL terminator, and the parser keeps doing *p (and strchr(..., *p)) without checking bounds.
- With a heap string of length N (bytes 0..N-1, N-1 is '\0'), the code evaluates *p when p == fmt+N (i.e., one past the allocated block), which is undefined behavior and trips ASan.


# Suggested Fix
In file_checkfmt():
- Carry the end pointer (fmt + strlen(fmt)) and guard every *p dereference:
```
const char *p = fmt, *end = fmt + strlen(fmt);
while (p < end && (c = *p++) != '\0') {
    if (c != '%') continue;

    while (p < end && strchr("#0.'+- ", *p)) p++;
    if (p < end && *p == '*') p++;
    while (p < end && isdigit((unsigned char)*p)) p++;
    if (p < end && *p == '.') { p++; while (p < end && isdigit((unsigned char)*p)) p++; }
    while (p < end && strchr("hljztL", *p)) p++;

    if (p >= end) return 1; // incomplete/invalid format
    switch (*p) { ... }
    p++;
}
```
- Alternatively, parse using a length-bounded approach (e.g., track remaining bytes) or bail out early on incomplete specifiers.


# To Reproduce

1. Compile libmagic with AddressSanitizer:
```
export CC="clang -fsanitize=address,fuzzer-no-link -g "
export CFLAGS="-g -O0"
mkdir build_asan
cd build_asan
../code/configure --prefix="$PWD/../bin_asan" --enable-static --disable-shared
make -j$(nproc)
make install

```

2. Compile the Fuzz Driver:

- Compile the fuzz driver with the following command: 
```
clang -g -O0 ./fuzz_driver_168.c -o ./fuzz_driver_168 -fsanitize=fuzzer,address,undefined -I../bin_asan/include ../bin_asan/lib/libmagic.a -llzma -lzstd -lz -lbz2
```

3. Run the Fuzz Driver:
```
./fuzz_driver_168 ./bug.input
```

# Desktop Environment
- OS and Version: Linux (Ubuntu 24.04.2 LTS)
- libmagic Version and Source: 
    main branch at commit aa12ab73697390f59cb4f622d70464775e31c657 (Wed Oct 22 19:39:12 2025 +0000)(from git@github.com:file/file.git)
- Compiler and Version: Ubuntu clang version 18.1.3


# ASan Report

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2463729613
INFO: Loaded 1 modules   (5334 inline 8-bit counters): 5334 [0x62db43a262c0, 0x62db43a27796), 
INFO: Loaded 1 PC tables (5334 PCs): 5334 [0x62db43a27798,0x62db43a3c4f8), 
./fuzz_driver_168: Running 1 inputs 1 time(s) each.
Running: ./bug.input
=================================================================
==1938837==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50b0000000a5 at pc 0x62db4396b8e6 bp 0x7ffe716f83b0 sp 0x7ffe716f83a8
READ of size 1 at 0x50b0000000a5 thread T0
    #0 0x62db4396b8e5 in file_checkfmt /root/cov_fuzz/testTP/libmagic/build_asan/src/../../code/src/funcs.c:103:28
    #1 0x62db4396c3f2 in file_vprintf /root/cov_fuzz/testTP/libmagic/build_asan/src/../../code/src/funcs.c:142:6
    #2 0x62db4396cdad in file_printf /root/cov_fuzz/testTP/libmagic/build_asan/src/../../code/src/funcs.c:182:7
    #3 0x62db439325a0 in drive_libmagic_with_format /root/cov_fuzz/testTP/libmagic/file_checkfmt/./fuzz_driver_168.c:45:5
    #4 0x62db439319ea in LLVMFuzzerTestOneInput /root/cov_fuzz/testTP/libmagic/file_checkfmt/./fuzz_driver_168.c:77:5
    #5 0x62db4383efc4 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x7cfc4) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #6 0x62db438280f6 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x660f6) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #7 0x62db4382dbaa in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x6bbaa) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #8 0x62db43858366 in main (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x96366) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #9 0x79dbb18a91c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #10 0x79dbb18a928a in __libc_start_main csu/../csu/libc-start.c:360:3
    #11 0x62db43822cc4 in _start (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x60cc4) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)

0x50b0000000a5 is located 0 bytes after 101-byte region [0x50b000000040,0x50b0000000a5)
allocated by thread T0 here:
    #0 0x62db438f30f3 in malloc (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x1310f3) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #1 0x62db43931c09 in build_format_string /root/cov_fuzz/testTP/libmagic/file_checkfmt/./fuzz_driver_168.c:22:25
    #2 0x62db43931894 in LLVMFuzzerTestOneInput /root/cov_fuzz/testTP/libmagic/file_checkfmt/./fuzz_driver_168.c:67:17
    #3 0x62db4383efc4 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x7cfc4) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #4 0x62db438280f6 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x660f6) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #5 0x62db4382dbaa in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x6bbaa) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #6 0x62db43858366 in main (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x96366) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)
    #7 0x79dbb18a91c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #8 0x79dbb18a928a in __libc_start_main csu/../csu/libc-start.c:360:3
    #9 0x62db43822cc4 in _start (/root/cov_fuzz/testTP/libmagic/file_checkfmt/fuzz_driver_168+0x60cc4) (BuildId: 53bdc1f229f256d5de404ec5f23e6fb001f4e160)

SUMMARY: AddressSanitizer: heap-buffer-overflow /root/cov_fuzz/testTP/libmagic/build_asan/src/../../code/src/funcs.c:103:28 in file_checkfmt
Shadow bytes around the buggy address:
  0x50affffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x50affffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x50afffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x50afffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x50b000000000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
=>0x50b000000080: 00 00 00 00[05]fa fa fa fa fa fa fa fa fa 00 00
  0x50b000000100: 00 00 00 00 00 00 00 00 00 00 04 fa fa fa fa fa
  0x50b000000180: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
  0x50b000000200: fd fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50b000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50b000000300: 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
==1938837==ABORTING
