bugfix: harden callgate buffer bounds
What changed, and why it matters
This commit fixes a buffer-validation bug in the COLDCARD Mk4 bootloader's secure callgate. The old code checked memory ranges using simple addition (address + length), which can overflow and trick the device into treating an attacker-controlled buffer as safe. The patch replaces those checks with subtraction-based math that cannot overflow, and also rejects negative input lengths. A security review by Karma-X is credited with the finding.
Treat this as a security fix and include it in the next firmware release. Users on Mk4 should update when the release is available. Developers should audit other bootloader callgates for similar pointer-arithmetic overflow patterns and consider static analysis for unsigned-integer wraparound in bounds checks.
Security signals we found
Integer overflow in pointer+bounds arithmetic
Out-of-range memory access through trusted callgate
Bootloader-level input validation bypass
Negative length accepted as valid input
Third-party security review finding credited (Karma-X)
Evidence from the diff
In stm32/mk4-bootloader/dispatch.c, the bootloader’s firewall_dispatch() and good_addr() validate caller-supplied buffers before trusted code acts on them. Previously, bounds checks such as (x+len) <= BL_SRAM_BASE and (x - FIRMWARE_START) < FW_MAX_LENGTH_MK4 could wrap around when len is large, causing an out-of-bounds or attacker-influenced pointer to pass validation. The patch adds a range_is_inside() helper that computes offset = addr - base and then checks offset < size && len <= (size - offset), eliminating the integer overflow. It also moves the NULL and minlen checks outside the minlen conditional and rejects len_in < 0 in firewall_dispatch(). A related output-size requirement for callgate method 25 is tightened from 8 bytes to 3*sizeof(uint32_t).
Changed components
stm32/mk4-bootloader/dispatch.cCOLDCARD Mk4 bootloader callgate / firewall_dispatchSRAM and firmware-flash buffer validationInspect captured patch +22 / −9
### releases/Next-ChangeLog.md
@@ -15,6 +15,8 @@ This lists the new changes that have not yet been published in a normal release.
Thanks to [@instagibbs](https://github.com/instagibbs) for reporting this issue.
- Bugfix: Fixed PSBT uploads being mistaken for partial firmware uploads.
- Bugfix: Prevent valid message signatures when using a Delta Mode PIN.
+- Bugfix: Harden callgate buffer validation against integer overflow and out-of-range access,
+ following a finding in the [Karma-X security review](https://karma-x.io/blog/post/75/).
# Mk Specific Changes
### stm32/mk4-bootloader/dispatch.c
@@ -33,19 +33,30 @@
#include "stm32l4xx_hal.h"
+// range_is_inside()
+//
+ static bool
+range_is_inside(uint32_t addr, uint32_t len, uint32_t base, uint32_t size)
+{
+ if(addr < base) return false;
+
+ uint32_t offset = addr - base;
+
+ // Subtraction-based bounds checks avoid overflow in addr + len.
+ return (offset < size) && (len <= (size - offset));
+}
+
// good_addr()
//
static int
good_addr(const uint8_t *b, int minlen, int len, bool readonly)
{
uint32_t x = (uint32_t)b;
- if(minlen) {
- if(!b) return EFAULT; // gave no buffer
- if(len < minlen) return ERANGE; // too small
- }
-
- if((x >= SRAM1_BASE) && ((x+len) <= BL_SRAM_BASE)) {
+ if(!b) return EFAULT; // gave no buffer
+ if(len < minlen) return ERANGE; // too small (also rejects negative lengths)
+
+ if(range_is_inside(x, (uint32_t)len, SRAM1_BASE, BL_SRAM_BASE - SRAM1_BASE)) {
// ok: it's inside the SRAM areas, up to where we start
return 0;
}
@@ -54,7 +65,7 @@ good_addr(const uint8_t *b, int minlen, int len, bool readonly)
return EPERM;
}
- if((x >= FIRMWARE_START) && (x - FIRMWARE_START) < FW_MAX_LENGTH_MK4) {
+ if(range_is_inside(x, (uint32_t)len, FIRMWARE_START, FW_MAX_LENGTH_MK4)) {
// inside flash of main firmware (happens for QSTR's)
return 0;
}
@@ -111,7 +122,7 @@ firewall_dispatch(int method_num, uint8_t *buf_io, int len_in,
// - mpy may provide a pointer to flash if we give it a qstr or small value, and if
// we're reading only, that's fine.
- if(len_in > 1024) { // arbitrary max, increase as needed
+ if((len_in < 0) || (len_in > 1024)) { // arbitrary max, increase as needed
rv = ERANGE;
goto fail;
}
@@ -565,7 +576,7 @@ firewall_dispatch(int method_num, uint8_t *buf_io, int len_in,
case 25: {
// mk4: usage of mcu key slots
- REQUIRE_OUT(8);
+ REQUIRE_OUT(3 * sizeof(uint32_t));
int *avail = (int *)(buf_io+0);
int *consumed = (int *)(buf_io+4);Why this scored 70/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.