[MacPorts] #74226: outguess: intermittent SIGBUS/crash during JPEG writing caused by config.h's obsolete #define size_t unsigned
MacPorts
noreply at macports.org
Thu Jul 16 03:53:07 UTC 2026
#74226: outguess: intermittent SIGBUS/crash during JPEG writing caused by
config.h's obsolete #define size_t unsigned
------------------------+--------------------
Reporter: HybridBits | Owner: (none)
Type: defect | Status: new
Priority: Normal | Milestone:
Component: ports | Version:
Keywords: | Port:
------------------------+--------------------
'''Port:''' outguess
'''Related to:''' #74183 (separate bug, same port -- see note at the end)
'''macOS version:''' macOS Sequoia 15.7.7 and macOS Tahoe 26.2 (reproduced
on both)
'''Xcode version:''' 16.2
'''Architecture:''' arm64 (Apple Silicon)
== Summary ==
outguess intermittently crashes with SIGBUS / EXC_BAD_ACCESS
(KERN_PROTECTION_FAILURE) while writing the compressed JPEG output,
leaving a 0-byte output file. The root cause is a stale, unconditional
macro in config.h:
{{{
/* Define to `unsigned' if <sys/types.h> doesn't define. */
#define size_t unsigned
}}}
This autoheader-generated line's own comment describes conditional intent
("if <sys/types.h> doesn't define it") but the actual generated macro has
no guard around it at all. On any modern 64-bit platform, <sys/types.h>
already provides a correct, 8-byte size_t -- so this macro unconditionally
and silently overrides it with a 4-byte unsigned throughout any file that
includes config.h.
jpg.c includes config.h. The bundled/patched libjpeg (jpeg-6b-steg) does
not. The result: jpg.c and the JPEG library disagree about the layout of
struct jpeg_destination_mgr -- specifically its size_t free_in_buffer
field, declared correctly as 8 bytes in jpeglib.h, but compiled as 4 bytes
wherever jpg.c's copy of config.h has already redefined the size_t token
before jpeglib.h gets included.
Concretely: jpg.c's init_destination() writes dest->pub.free_in_buffer =
BUFSIZE; (256) as a 4-byte store. Code inside the JPEG library (e.g.
jcmarker.c's emit_byte()) reads that same field back as a genuine 8-byte
size_t, picking up 4 bytes of uninitialized garbage in the high half along
with the correct 256 in the low half. The resulting value is enormous, so
the buffer-refill check (if (--dest->free_in_buffer == 0)) never fires,
and the output pointer walks past the end of its 256-byte scratch buffer
until it eventually hits unmapped/read-only memory and crashes.
== How this was found ==
This surfaced while investigating a report of intermittent crashes and
0-byte output specifically when the -e (error-correcting encoding) flag
was combined with a from-source arm64 rebuild -- worse on Sequoia than on
Tahoe, consistent with ASLR-dependent memory layout affecting whether the
eventual out-of-bounds write happens to land somewhere fatal. Under an
AddressSanitizer-instrumented build it reproduced 100% reliably (20/20
runs), which made it possible to trace precisely via lldb:
A conditional watchpoint on cinfo->dest->next_output_byte (breaking only
when the pointer left the legitimate 256-byte buffer range) caught the
pointer walking one byte past the end.
cinfo->dest->free_in_buffer printed as an enormous garbage value instead
of 256, and was already wrong immediately after the assignment that's
supposed to set it -- not corrupted later.
A raw memory dump (x/2gx) at that address showed the low 4 bytes correctly
holding 256 and the high 4 bytes as untouched poison -- direct proof of a
32-bit store into what should be an 8-byte field.
Comparing the literal preprocessed struct definition each file's compiler
produces (cc -E ... | grep -A20 "struct jpeg_destination_mgr") confirmed
it directly: jpg.c compiles the field as unsigned free_in_buffer;, while
jcmarker.c (inside the JPEG library) compiles the identical struct as
size_t free_in_buffer;.
Traced the disagreement to config.h's unconditional size_t override.
== Patch ==
Disable the obsolete override (kept as a comment, matching the file's
existing style, rather than deleted outright, to preserve the historical
context):
{{{
--- a/config.h
+++ b/config.h
@@ -12,7 +12,8 @@
/* Define if you have a working `mmap' system call. /
/ #undef HAVE_MMAP */
-/* Define to unsigned' if <sys/types.h> doesn't define. */ -#define
size_t unsigned +/* Define to unsigned' if <sys/types.h> doesn't define.
/
+/ DISABLED -- see ticket for full explanation. On any modern 64-bit
platform <sys/types.h> already provides a correct size_t; this
unconditional override was causing a real struct-layout mismatch
between jpg.c and the bundled JPEG library. /
+/ #define size_t unsigned */
/* Define if you have the ANSI C header files. /
/ #undef STDC_HEADERS */
}}}
== Testing ==
After applying this patch and rebuilding (both an ASan debug build and a
plain optimized release build):
Ran the same -e repro command that crashed 20/20 times before the fix, 20
times against the patched build: 20/20 succeeded cleanly, full successful
embeds each time, no crashes, no ASan errors.
Also confirmed against a plain, non-ASan optimized release build (not just
the debug/sanitizer build) -- same result, no crashes.
== Possible relevance to #74183 ==
This is a separate bug from #74183 (which is a stack buffer overflow in
steg_adjust_errors, unrelated code path, confirmed against the MacPorts-
packaged 0.4 build specifically). This ticket's bug was found and fixed
against the original 2001 0.2 source. It has not been checked whether the
0.3/0.4 lineage (which MacPorts packages) inherited the same config.h
size_t override -- it may be worth checking whether that shim is still
present there too, since if so this same fix would likely apply.
Happy to provide the full patched jpg.c/config.h, further test details, or
check the 0.4 source directly if that would help move this forward.
--
Ticket URL: <https://trac.macports.org/ticket/74226>
MacPorts <https://www.macports.org/>
Ports system for macOS
More information about the macports-tickets
mailing list