// Phase 6.2 UBSan-min smoke probe. // // Nine UB cases — one per recoverable handler kind we exercise: // kind 0 (sentinel 0xC0DE): add-overflow (i16 INT_MAX + 1) // kind 1 (sentinel 0xC0DF): shift-out-of-bounds (1 << 17 on a u16) // kind 2 (sentinel 0xC0E0): divrem-overflow (n / 0) // kind 3 (sentinel 0xC0E1): sub-overflow (INT_MIN - 1) // kind 4 (sentinel 0xC0E2): mul-overflow (INT_MAX * 2) // kind 5 (sentinel 0xC0E3): negate-overflow (-INT_MIN) // kind 6 (sentinel 0xC0E4): pointer-overflow (ptr + huge offset) // kind 7 (sentinel 0xC0E5): load-invalid-value (_Bool from byte=2) // kind 8 (sentinel 0xC0E6): out-of-bounds (arr[idx>=N]) // // The probe overrides each relevant `__ubsan_handle_*_minimal` recovering // handler with a strong definition that records its firing in a static // state byte. After each UB, the probe writes 0xC0DE+kind to a per-kind // 16-bit slot at 0x025000+kind*2 to prove (a) the instrumentation fired // and (b) execution recovered cleanly past the UB. The recover handler // returning normally is the whole point of -fsanitize-minimal-runtime // + -fsanitize-recover; this probe is what proves the round-trip. // // To verify all nine at once we cascade the sentinel writes through a // staircase of word stores so the smoke harness can read independent // 16-bit values back from MAME. // // Compile with -fsanitize=undefined -fsanitize-minimal-runtime. #include // Bank-2 BSS at $025000-$025014 -- outside the SHR shadow and outside // $C000-$CFFF IO window. link816 places .bss at the user-specified // --bss-base (we pass 0xA000) so these constant addresses are // independent of BSS layout. #define MARK_ADD_OVF ((volatile uint16_t *)0x025000UL) #define MARK_SHIFT_OOB ((volatile uint16_t *)0x025002UL) #define MARK_DIV_ZERO ((volatile uint16_t *)0x025004UL) #define MARK_SUB_OVF ((volatile uint16_t *)0x025006UL) #define MARK_MUL_OVF ((volatile uint16_t *)0x025008UL) #define MARK_NEG_OVF ((volatile uint16_t *)0x02500AUL) #define MARK_PTR_OVF ((volatile uint16_t *)0x02500CUL) #define MARK_LOAD_INVAL ((volatile uint16_t *)0x02500EUL) #define MARK_OUT_OF_BNDS ((volatile uint16_t *)0x025010UL) #define DONE_SENTINEL ((volatile uint16_t *)0x025012UL) // Strong overrides win over runtime/ubsan.o's weak-by-link defaults. // Each fires once per kind and records that the corresponding UB // instrumentation reached us. Recovering handlers MUST return so the // probe continues executing past the UB site. static volatile uint8_t handlerFiredAdd = 0; static volatile uint8_t handlerFiredShift = 0; static volatile uint8_t handlerFiredDiv = 0; static volatile uint8_t handlerFiredSub = 0; static volatile uint8_t handlerFiredMul = 0; static volatile uint8_t handlerFiredNeg = 0; static volatile uint8_t handlerFiredPtr = 0; static volatile uint8_t handlerFiredLoadInv = 0; static volatile uint8_t handlerFiredOob = 0; void __ubsan_handle_add_overflow_minimal(void) { handlerFiredAdd = 1; } void __ubsan_handle_shift_out_of_bounds_minimal(void) { handlerFiredShift = 1; } void __ubsan_handle_divrem_overflow_minimal(void) { handlerFiredDiv = 1; } void __ubsan_handle_sub_overflow_minimal(void) { handlerFiredSub = 1; } void __ubsan_handle_mul_overflow_minimal(void) { handlerFiredMul = 1; } void __ubsan_handle_negate_overflow_minimal(void) { handlerFiredNeg = 1; } void __ubsan_handle_pointer_overflow_minimal(void) { handlerFiredPtr = 1; } void __ubsan_handle_load_invalid_value_minimal(void) { handlerFiredLoadInv = 1; } void __ubsan_handle_out_of_bounds_minimal(void) { handlerFiredOob = 1; } // Each UB site goes through a noinline wrapper so the optimizer // cannot constant-fold the operation away. __attribute__((noinline)) // + volatile inputs blocks the obvious folding paths; we also wrap // the inputs with `volatile` reads so the LLVM mid-end has no // known-value to work with. __attribute__((noinline)) static int16_t triggerAddOverflow(int16_t a, int16_t b) { return a + b; } __attribute__((noinline)) static uint16_t triggerShiftOob(uint16_t a, uint16_t s) { return a << s; } __attribute__((noinline)) static int16_t triggerDivByZero(int16_t a, int16_t b) { return a / b; } __attribute__((noinline)) static int16_t triggerSubOverflow(int16_t a, int16_t b) { return a - b; } __attribute__((noinline)) static int16_t triggerMulOverflow(int16_t a, int16_t b) { return a * b; } __attribute__((noinline)) static int16_t triggerNegateOverflow(int16_t a) { return -a; } __attribute__((noinline)) static char *triggerPointerOverflow(char *p, int32_t o) { return p + o; } __attribute__((noinline)) static int triggerLoadInvalidValue(volatile uint8_t *p) { _Bool v = *(_Bool *)p; // Use the value so the load isn't dead-stripped. We don't trust // the post-instrumentation cast to a 0/1 narrow value -- the // important thing is the load itself fired the handler. return v ? 1 : 0; } __attribute__((noinline)) static int16_t triggerOutOfBounds(int16_t idx) { static int16_t arr[4] = { 10, 20, 30, 40 }; return arr[idx]; } int main(void) { // --- case 0: signed-overflow add (INT16_MAX + 1) --- volatile int16_t aMax = 0x7FFF; volatile int16_t aOne = 1; (void)triggerAddOverflow(aMax, aOne); if (handlerFiredAdd) { *MARK_ADD_OVF = 0xC0DE; } // --- case 1: shift-out-of-bounds (1 << 17 on a u16) --- volatile uint16_t base = 1; volatile uint16_t shf = 17; (void)triggerShiftOob(base, shf); if (handlerFiredShift) { *MARK_SHIFT_OOB = 0xC0DF; } // --- case 2: divide-by-zero (signed) --- volatile int16_t num = 42; volatile int16_t den = 0; (void)triggerDivByZero(num, den); if (handlerFiredDiv) { *MARK_DIV_ZERO = 0xC0E0; } // --- case 3: sub-overflow (INT16_MIN - 1) --- volatile int16_t aMin = (int16_t)0x8000; (void)triggerSubOverflow(aMin, aOne); if (handlerFiredSub) { *MARK_SUB_OVF = 0xC0E1; } // --- case 4: mul-overflow (INT16_MAX * 2 wraps) --- volatile int16_t aTwo = 2; (void)triggerMulOverflow(aMax, aTwo); if (handlerFiredMul) { *MARK_MUL_OVF = 0xC0E2; } // --- case 5: negate-overflow (-INT16_MIN) --- (void)triggerNegateOverflow(aMin); if (handlerFiredNeg) { *MARK_NEG_OVF = 0xC0E3; } // --- case 6: pointer-overflow (signed-wrap on i16 addr) --- // Cast a high address to char* and add a positive offset that // overflows the address calculation. -fsanitize=pointer-overflow // fires on signed-overflow of the offset add. volatile uint32_t hiAddr = 0xFFFFFFF0UL; volatile int32_t big = 0x40; char *p = (char *)(uintptr_t)hiAddr; (void)triggerPointerOverflow(p, big); if (handlerFiredPtr) { *MARK_PTR_OVF = 0xC0E4; } // --- case 7: load-invalid-value (_Bool from byte=2) --- volatile uint8_t boolByte = 2; (void)triggerLoadInvalidValue(&boolByte); if (handlerFiredLoadInv) { *MARK_LOAD_INVAL = 0xC0E5; } // --- case 8: out-of-bounds (static arr[idx>=N]) --- volatile int16_t badIdx = 7; (void)triggerOutOfBounds(badIdx); if (handlerFiredOob) { *MARK_OUT_OF_BNDS = 0xC0E6; } // Final liveness sentinel -- only written if we got past all nine // UB sites without the runtime aborting (which would have spun on // a BRK_pseudo at $70 instead of reaching here). *DONE_SENTINEL = 0xC0DA; // Halt -- crt0's return-from-main path hits a BRK that headless // MAME wild-jumps from, so spin-wait instead. while (1) { } }