Fix RAM overflow from a crafted TZRULES.BIN, plus seven smaller bounds / ISR-safety bugs#7
Draft
peterlewis wants to merge 2 commits into
Draft
Fix RAM overflow from a crafted TZRULES.BIN, plus seven smaller bounds / ISR-safety bugs#7peterlewis wants to merge 2 commits into
peterlewis wants to merge 2 commits into
Conversation
- loadRules: reject untrusted rowLength/numEntries from TZRULES.BIN (RAM overflow) - MODE_TEXT: clamp snprintf untruncated-return index (uart2_tx_buffer OOB) - mk4-date latchDisplay: bound dp_pos per orientation (buffer_a/b OOB) - readConfigFile: don't treat a zero FAT timestamp as a cache hit (first-boot hang) - config-over-USB: defer postConfigCleanup out of the USB ISR (sendDate/nextMode races) - firmwareCheckOnEject: fatfs_busy guard for FATFS reentrancy on USB eject
Two further pre-existing bugs from the same read-through. - mk4-time NMEA-over-CDC: CDC_Copy_Transmit() / CDC_Transmit_FS() dereference hUsbDeviceFS.pClassDataCDC unconditionally, but it is NULL until a USB host enumerates. The default nmea_cdc_level = NMEA_ALL forwards every GPS sentence to CDC, so on charger-only power the first forwarded sentence faults in ISR context. NULL guard on both, plus a length clamp before the memcpy. - mk4-date CMD_LOAD_TEXT: 'if (text_idx > MAX_TEXT_LEN) return;' passes at text_idx == MAX_TEXT_LEN and writes text[32], one past the 32-byte array. '>' -> '>='. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 6, 2026
mk4-time/main.c conflicted whole-file (EOL mismatch between the PR branches); resolved to the validated rollup integration. The final integration-delta commit records the exact residue of the whole union against the PR branches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 6, 2026
The exact residue between the pure union of the five draft PRs (mitxela#5 $PMTXTS, mitxela#6 astro pack, mitxela#7 hardening, mitxela#8 sidereal/solar, mitxela#9 tempcomp + significance-fade + tc_seed — mitxela#5 and mitxela#6 arrive as ancestors of mitxela#9 and mitxela#8) and the rollup tree that has been emulator-verified and hardware-run: - astro.c/astro.h/test_astro.c: rollup astro refinements not yet folded back into the refreshed PR mitxela#6/mitxela#8 heads (candidates for a future PR update) - version.c: rollup version string - qspi/output/*.bin: rollup's built firmware images - .settings IDE noise After this commit the megapack tree is BYTE-IDENTICAL to the previously validated rollup (feaf970) while the merge ancestry proves containment of all five PR heads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The fixes
loadRules()TZRULES.BINfirmwareCheckOnEject()sendDate()MODE_TEXTuart2_tx_buffer[32](time board)latchDisplay()(date board)dp_poswrites outsidebuffer_a/b[]rxConfigString()nextMode()/sendDate()in the ISRreadConfigFile()config.txtis never loaded → first-boot hangCDC_Copy_Transmit()/CDC_Transmit_FS()CMD_LOAD_TEXT(date board)text[32]1 —
loadRules()RAM overflowTZRULES.BINlives on the FAT volume the clock exposes over USB MSC, so its bytes are host-writable.loadRules()readsrowLengthandnumEntries(each auint8_t) straight from the file, then runsfor (i=0; i<numEntries; i++) f_read(&rules[i], rowLength, …)into the fixedrules[MAX_RULES](162 × 8 B) with no check. With both bytes maxed at 255 a malformed file writes ~990 bytes past the end of the array into adjacent globals — and it does so at cold boot and on every zone re-lookup, so it faults reliably.Fix: reject the file (
RULES_HEADER_ERR) whenrowLength > sizeof rules[0]ornumEntries > MAX_RULES, before the read loop. Valid files are unaffected.Repro: on the mounted MSC volume, set the
rowLengthbyte (file offset 4, normally 8) to0xFF— or any zone'snumEntriesto> 162— then trigger a zone load (power-cycle, or move position so the timezone is re-looked-up).2 —
firmwareCheckOnEject()FATFS re-entrancyThis runs from the USB MSC eject command, i.e. in the USB OTG ISR, and calls FATFS (
f_open/f_read). It already guards withif (QSPI_Locked()) { delayedCheckOnEject=1; return; }, which defers the whole check to the main loop when it sees QSPI contention — so the design isn't naive. The gap is thatQSPI_Locked()is only true during a QSPI transfer; in the lulls between transfers within a single main-loop FATFS call it reads false, the deferral doesn't fire, and the ISR re-enters non-reentrant FATFS anyway (corrupting FATFS state / risking a spurious reset).Fix: a
volatile fatfs_busyflag set around the main-loop FATFS regions; the guard becomesif (fatfs_busy || QSPI_Locked()). It's set/cleared only at the main-loop level, so it stays balanced and can't get stuck and block a legitimate firmware-update-on-eject.3 —
MODE_TEXTone-byte overrun (time board)i = snprintf((char*)&uart2_tx_buffer[1], 30, "%s", textDisplay)—snprintfreturns the length it would have written, not the truncated count. A 31-charTEXT=(the parser caps the value at 31) leavesi == 31, so the followinguart2_tx_buffer[++i] = …writes[32], one past the 32-byte buffer, and the DMA length reads one byte OOB. Self-inflicted via config and only one byte, hence Minor — but easy to close.Fix:
if (i > 29) i = 29;— clamp to the bytes thesnprintfactually wrote.4 — date-board
dp_posout-of-boundsdp_poscomes fromtext_idx, which the inter-board UART stream can advance past the 10-digit display.latchDisplay()then indexesbuffer_a[5]/buffer_b[5]with9-dp_pos(goes negative when the display is inverted) anddp_pos-6(overruns when it isn't) → OOB writes into adjacent RAM.Fix: bail out at the top of the decimal-point block when
dp_posis out of range for the current orientation (> 9inverted,> 10otherwise), so neither expression can leave[0,4].5 — config-over-USB runs
nextMode()/sendDate()in the ISRrxConfigString()runs in the USB OTG ISR (CDC). On each completed line it calledpostConfigCleanup(), which callsnextMode()andsendDate(). The priority-0 SysTick repaint preempts the USB ISR and callssendDate(0)directly — sosendDate()is genuinely re-entered — andnextMode()mutates display state (displayMode, the segment buffers) that the repaint reads, so it races too. Result: a torn date row or inconsistent mode state.Fix: defer
postConfigCleanup()to the main loop via a newdelayedPostConfigCleanupflag. The file-config path already runspostConfigCleanup()in thread context (becausereadConfigFileitself is the thing deferred to the main loop); this gives the USB-CDC path the same thread-context guarantee.6 — zero-timestamp
config.txt→ first-boot hangconfigis{0}-initialised, so thefdate/ftimemtime-cache key starts at 0. Aconfig.txtwritten while the volume's FAT clock was unset carries a zero timestamp, which matches that initial key on the very first boot →readConfigFile()returns early → config is never applied → no display mode is enabled → the first MODE-button press spinsnextMode()forever (there's no watchdog).Fix: never treat a zero stamp as a cache hit —
if ((fno.fdate || fno.ftime) && fno.fdate==config.fdate && fno.ftime==config.ftime) return;.7 — NMEA-over-CDC NULL deref → hard-fault on charger-only power
CDC_Copy_Transmit()and stockCDC_Transmit_FS()dereferencehUsbDeviceFS.pClassDataCDCunconditionally, but it is NULL until a USB host enumerates. With the defaultnmea_cdc_level = NMEA_ALL, every GPS sentence is forwarded to CDC, so on charger-only power — no host — the first forwarded sentence dereferences NULL in ISR context and hard-faults. This is the most common deployment (a dumb USB charger), so it faults reliably there.Fix: a NULL guard at the top of both functions (return cleanly when not enumerated), plus a length clamp before the
memcpyinto the static tx buffer.mk4-time/USB_DEVICE/App/usbd_cdc_if.c.8 — date-board
CMD_LOAD_TEXTone-byte OOBDistinct from #3 (that's
uart2_tx_bufferon the time board; this is thetext[]buffer on the date board).CMD_LOAD_TEXTbounds withif (text_idx > MAX_TEXT_LEN) return;then writestext[text_idx++] = x;. Attext_idx == MAX_TEXT_LEN(32) the guard passes and it writestext[32], one past the 32-byte array, into the adjacent global. Reachable with a 32-plus-char text-mode string.Fix:
>→>=.mk4-date/Core/Src/main.c.Verification
TZRULES.BIN), so the per-bug case above rests on the code, not on a hardware repro of each fault.TZRULES.BIN/config.txtstill loads;fatfs_busyis balanced so it can't latch on).Files
mk4-time/Core/Src/main.c— fixes 1, 3, 5, 6, plus thefatfs_busywraps for 2mk4-time/Core/Src/chainloader.candCore/Inc/main.h— fix 2mk4-time/USB_DEVICE/App/usbd_cdc_if.c— fix 7mk4-date/Core/Src/main.c— fixes 4, 8