
CVE-2026-39259
Project: https://github.com/alexfru/SmallerC
SmallerC's scanf implementation does not enforce an upper bound on string reads when %s or %[ is used without an explicit field width in the format string The runtime will continue writing into the destination buffer until it encounters whitespace or EOF regardless of the buffer's actual allocated size Any bytes past the boundary land directly on the stack overwriting whatever the compiler placed above the buffer locals saved registers the return address
This is not a novel vulnerability class Unbounded scanf string reads have been documented since the early days of C and any competent static analysis tool will flag them What makes this worth reporting in the context of SmallerC specifically is the target environment SmallerC is designed for DOS and bare-metal embedded targets platforms that by definition do not provide stack canaries ASLR NX bits or any of the mitigations that make exploitation difficult on modern systems The same primitive that would require a significant research effort to turn into a working exploit on a hardened Linux binary becomes considerably more tractable on a DOS program running on a flat predictable stack
The buffer is 16 bytes The input is 20 non-whitespace bytes The %s conversion has no width specifier so sscanf reads all 20 bytes plus a null terminator 21 bytes total into a 16-byte allocation The 5 bytes past the boundary corrupt adjacent stack memory What exactly gets corrupted depends on the compiler's stack layout decisions for that particular function but the overwrite itself is deterministic and unconditional every time this code path runs with this input
Proof of Concept
#include <stdioh>
#include <stringh>
/*
* Build with SmallerC targeting DOS or bare-metal
* Demonstrates unbounded %s write past a fixed stack buffer
*
* buffer is 16 bytes payload is 20 non-whitespace bytes
* sscanf writes 21 bytes (20 + null terminator) into buffer
* corrupting 5 bytes of adjacent stack memory
*
* To observe the corruption inspect stack memory after the call:
* the 5 bytes immediately above buffer will contain 'A' (0x41)
*/
int main() {
char buffer[16];
char canary[8];
memset(buffer 0x00 sizeof(buffer));
memset(canary 0xCC sizeof(canary)); /* marker to detect overwrite */
printf("[*] canary before: ");
for (int i = 0; i < 8; i++) printf("%02x " (unsigned char)canary[i]);
printf("\n");
sscanf("AAAAAAAAAAAAAAAAAAAA" "%s" buffer); /* 20 bytes into 16-byte buffer */
printf("[*] canary after: ");
for (int i = 0; i < 8; i++) printf("%02x " (unsigned char)canary[i]);
printf("\n");
if (memcmp(canary "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC" 8) != 0)
printf("[!] stack corruption confirmed canary overwritten\n");
else
printf("[-] canary intact (stack layout placed it elsewhere)\n");
return 0;
}
Expected output on an affected build:
[*] canary before: cc cc cc cc cc cc cc cc
[*] canary after: 41 41 41 41 41 cc cc cc
[!] stack corruption confirmed canary overwritten
The canary placement relative to buffer depends on the compiler's stack layout If the output shows the canary intact the overwrite is still happening it's landing on something else above the buffer Adjust the reproducer by inspecting the actual stack frame with a debugger to locate where the 5 corrupt bytes land
A correction worth making explicitly: some reports in this vulnerability class attempt to demonstrate return address control by appending a target address after a null byte in the payload like "AAAAAAAAAAAAAAAAAAA\x00\x90\x04\x08" This does not work The %s conversion in sscanf treats \x00 as a string terminator and stops reading immediately when it encounters it The bytes following the null byte are never processed Demonstrating actual return address control requires delivering the overwrite without a null byte in the critical portion of the payload which in turn requires knowing the exact stack layout of the target binary the distance from the buffer to the saved return address whether the compiler inserted any padding and what alignment constraints apply None of that falls out of this reproducer automatically
What the reproducer does establish cleanly is the corruption primitive itself The out-of-bounds write is real reproducible and not dependent on any race condition or timing On a DOS or embedded target where the stack layout is static and predictable across builds bridging from this primitive to a working exploit is a realistic research effort rather than a theoretical exercise
The affected scenario is narrow but not contrived A program has to be built with SmallerC use scanf-family parsing with an unbounded %s or %[ specifier write into a fixed-size stack buffer and accept input from a source the attacker can influence All four conditions have to hold simultaneously Programs that use correct field widths %15s for a char[16] are not affected Programs that don't parse attacker-controlled input are not affected The issue is a defect in how SmallerC's runtime handles the missing width constraint but it only becomes a security concern when application code exposes that defect to untrusted input
On the application side the fix is straightforward: specify a field width that leaves room for the null terminator %15s for a 16-byte buffer %63s for a 64-byte buffer This is standard C practice and fully supported by the format string syntax On the SmallerC project side the more durable work is adding regression tests that cover both bounded and unbounded %s and %[ behavior across scanf sscanf and fscanf verifying that explicit field widths are actually honored in the implementation and documenting the unsafe pattern prominently A compiler-level diagnostic that warns when %s or %[ appears without a field width in a format string literal would prevent this class of mistake proactively and would be a meaningful addition to the toolchain
Severity is Medium when external input reaches the vulnerable code path It drops to Low when the input is local or non-privileged The target environment specifically the absence of modern exploit mitigations on SmallerC's intended platforms is what separates this from a generic "don't use unbounded scanf" advisory and makes it worth reporting at the project level rather than treating it purely as application-layer misuse
Credit: Yousif Wazni