build: only generate deltas that are 1/2 the firmware size or less
What changed, and why it matters
This commit changes the build tooling that creates over-the-air (OTA) firmware update patches for the Blockstream Jade hardware wallet. It now refuses to generate a delta patch if the patch would be larger than half the size of the full new firmware, unless the caller explicitly uses a new '--force' flag. The CI scripts were updated to pass '--force' for their existing self-to-self test patches, so those tests still run. The change is a build/operational hardening measure: it prevents the release pipeline from shipping oversized delta updates that could be inefficient or potentially problematic, but it does not by itself fix a runtime vulnerability in the device.
Treat as a defensive hardening improvement. Review whether the 50% threshold is appropriate for all release channels, ensure '--force' is not used in production release scripts, and confirm the device-side OTA installer handles missing or oversized delta files gracefully (e.g., by falling back to a full firmware update). No urgent security patch is indicated by this commit alone.
Security signals we found
Build pipeline now enforces a size policy on delta firmware patches
New '--force' escape hatch added for intentional oversized patch generation
CI scripts updated to preserve existing self-to-self patch smoke tests
No changes to device-side OTA verification, signature checks, or flash routines
Evidence from the diff
tools/mkpatch.py’s create_patch() now computes the full target firmware size and the uncompressed bsdiff patch size, and only writes the compressed patch file when fw_patch_size * 2 <= fw_full_size, or when a new ‘force’ argument is True. The CLI gained an optional ‘–force’ argument. The two CI scripts (qemu_ci_flash.sh and ota_delta_ci.sh) were updated to pass ‘–force’ because their smoke-test patches are generated from a firmware image to itself, which can produce large or pathological bsdiff outputs. This is a supply-chain/build-hardening change rather than a patch for an exploitable runtime bug in the firmware.
Changed components
tools/mkpatch.pymain/qemu/qemu_ci_flash.shota_delta_ci.shInspect captured patch +24 / −15
diff --git a/main/qemu/qemu_ci_flash.sh b/main/qemu/qemu_ci_flash.sh
index 195b4bf..5f6e3b6 100755
--- a/main/qemu/qemu_ci_flash.sh
+++ b/main/qemu/qemu_ci_flash.sh
@@ -45,7 +45,7 @@ FW_FULL=$(ls build/*_fw.bin)
python jade_ota.py --log=INFO --skipble --serialport=tcp:localhost:30121 --fwfile=${FW_FULL}
# Flash a simple patch-to-self, just to smoke test ota-delta
-./tools/mkpatch.py ${FW_FULL} ${FW_FULL} build/
+./tools/mkpatch.py ${FW_FULL} ${FW_FULL} build/ --force
FW_PATCH=$(ls ./build/*_patch.bin)
cp "${FW_FULL}.hash" "${FW_PATCH}.hash"
python jade_ota.py --log=INFO --skipble --serialport=tcp:localhost:30121 --fwfile=${FW_PATCH}
diff --git a/ota_delta_ci.sh b/ota_delta_ci.sh
index cf4a84c..9198e43 100755
--- a/ota_delta_ci.sh
+++ b/ota_delta_ci.sh
@@ -32,9 +32,9 @@ FW_NORADIO=$(ls build_noradio/*_noradio_*_fw.bin)
PATCHDIR=patches
mkdir -p ${PATCHDIR}
-./tools/mkpatch.py ${FW_NORADIO} ${FW_NORADIO} ${PATCHDIR}
-./tools/mkpatch.py ${FW_BLE} ${FW_BLE} ${PATCHDIR}
-./tools/mkpatch.py ${FW_NORADIO} ${FW_BLE} ${PATCHDIR} # makes both directions
+./tools/mkpatch.py ${FW_NORADIO} ${FW_NORADIO} ${PATCHDIR} --force
+./tools/mkpatch.py ${FW_BLE} ${FW_BLE} ${PATCHDIR} --force
+./tools/mkpatch.py ${FW_NORADIO} ${FW_BLE} ${PATCHDIR} --force # makes both directions
sleep 2
# first we ota to noradio via ble
diff --git a/tools/mkpatch.py b/tools/mkpatch.py
index 72894b3..c997884 100755
--- a/tools/mkpatch.py
+++ b/tools/mkpatch.py
@@ -39,7 +39,7 @@ def write_decompressed(compressedpath, uncompressedpath):
return fwtools.write(uncompressed, uncompressedpath)
-def create_patch(frominfo, frompath, toinfo, topath, outputdir):
+def create_patch(frominfo, frompath, toinfo, topath, outputdir, force):
tmppathpatch = _tmpfilepath(outputdir, 'patch')
try:
@@ -49,17 +49,25 @@ def create_patch(frominfo, frompath, toinfo, topath, outputdir):
rslt = subprocess.run([bsdiff, frompath, topath, tmppathpatch])
assert rslt.returncode == 0
- # Read the uncompressed patch data, and write zlib compressed
- # with the standard expected filename
+ with open(topath, 'rb') as f:
+ fw_full_size = len(f.read())
+
+ # Read the uncompressed patch data,
patch = fwtools.read(tmppathpatch)
- patchpath = fwtools.get_patch_compressed_filepath(patch, frominfo, toinfo, outputdir)
- compressed = fwtools.compress(patch)
- fwtools.write(compressed, patchpath)
+ fw_patch_size = len(patch)
+ if force or (fw_patch_size * 2 <= fw_full_size):
+ # Patch is 50% or less of the full size so create it
+ # (zlib compressed, with the standard expected filename)
+ patchpath = fwtools.get_patch_compressed_filepath(patch, frominfo, toinfo, outputdir)
+ compressed = fwtools.compress(patch)
+ fwtools.write(compressed, patchpath)
+ else:
+ logger.info(f'skipping oversize delta {fw_full_size}->{fw_patch_size}')
finally:
_remove(tmppathpatch)
-def create_patches(fwpathA, fwpathB, outputdir):
+def create_patches(fwpathA, fwpathB, outputdir, force):
logger.info(f'Patching between {fwpathA} and {fwpathB}')
typeA, infoA, infoA_ = fwtools.parse_compressed_filename(fwpathA)
@@ -76,8 +84,8 @@ def create_patches(fwpathA, fwpathB, outputdir):
write_decompressed(fwpathB, tmppathB)
# Create patches in both directions
- create_patch(infoA, tmppathA, infoB, tmppathB, outputdir)
- create_patch(infoB, tmppathB, infoA, tmppathA, outputdir)
+ create_patch(infoA, tmppathA, infoB, tmppathB, outputdir, force)
+ create_patch(infoB, tmppathB, infoA, tmppathA, outputdir, force)
finally:
# Delete the uncompressed firmware images
_remove(tmppathA)
@@ -91,10 +99,11 @@ if __name__ == '__main__':
jadehandler = logging.StreamHandler()
logger.addHandler(jadehandler)
- assert len(sys.argv) == 4, f'Usage: {sys.argv[0]} fwA fwB output_dir'
+ assert len(sys.argv) in (4, 5), f'Usage: {sys.argv[0]} fwA fwB output_dir [--force]'
# Compressed firmware (ie. input) files to patch between
fwA, fwB, outputdir = sys.argv[1], sys.argv[2], sys.argv[3]
+ force = len(sys.argv) == 5 and sys.argv[4] == '--force'
for fw in [fwA, fwB]:
assert os.path.exists(fw) and os.path.isfile(fw), f'Firmware file {fw} not found.'
@@ -108,4 +117,4 @@ if __name__ == '__main__':
os.linesep + 'gcc -O2 -DBSDIFF_EXECUTABLE -o tools/bsdiff components/esp32_bsdiff/bsdiff.c'
# Create patches between firmware files
- create_patches(fwA, fwB, outputdir)
+ create_patches(fwA, fwB, outputdir, force)
Why this scored 26/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.