F-4789: fix uint64_t overflow in squashelf range filter segment end

In the PT_LOAD range filter, segmentEnd was computed as
p_paddr + p_memsz - 1 in uint64_t with no overflow check. Both fields
come straight from the (possibly crafted) ELF64 program header. When
p_paddr + p_memsz exceeds 2^64 the result wraps below p_paddr: the
wrapped end can land back inside the requested range, so a segment whose
true span lies entirely outside the range is spuriously included (and the
converse can silently drop a valid segment). squashelf would then emit a
squashed image that wolfBoot signs and boots.

Detect the overflow before computing segmentEnd and treat such a segment
as out-of-range. ELF32 cannot overflow a uint64 sum and is unaffected.

Adds tools/squashelf/test-range-overflow.py (run via `make test`) which
fails before the fix and passes after.
pull/791/head
Daniele Lacamera 2026-06-09 10:24:56 +02:00
parent 740383a3f5
commit f193b9c239
4 changed files with 129 additions and 2 deletions

1
.gitignore vendored
View File

@ -197,6 +197,7 @@ tools/squashelf/**
!tools/squashelf/squashelf.c
!tools/squashelf/Makefile
!tools/squashelf/README.md
!tools/squashelf/test-range-overflow.py
# Generated configuration files
.config

View File

@ -18,13 +18,16 @@ else
CFLAGS+=$(OPTIMIZE)
endif
.PHONY: clean all debug
.PHONY: clean all debug test
all: $(TARGET)
debug: CFLAGS+=$(DEBUG_FLAGS)
debug: all
test: $(TARGET)
python3 test-range-overflow.py ./$(TARGET)
$(TARGET): $(TARGET).o
@echo "Building squashelf tool"
$(CC) -o $@ $< $(LDFLAGS) $(CFLAGS_EXTRA)

View File

@ -560,7 +560,27 @@ int main(int argCount, char** argValues)
/* Apply range filter if specified */
if (hasRange) {
uint64_t segmentStart = p_paddr;
uint64_t segmentEnd = p_paddr + p_memsz - 1;
uint64_t segmentEnd;
/* Guard against uint64_t overflow when computing the segment
* end (CWE-190). Both fields come straight from the (possibly
* crafted) program header; if p_paddr + p_memsz - 1 wrapped, the
* range check could spuriously include an out-of-range segment
* or drop a valid one. Treat such a segment as out-of-range. */
if (p_memsz == 0) {
segmentEnd = p_paddr;
}
else if (p_paddr > UINT64_MAX - (p_memsz - 1)) {
fprintf(stderr,
"Skipping segment %zu (LMA 0x%lx, size 0x%lx) - "
"address range overflows 64-bit space\n",
i, (unsigned long)p_paddr,
(unsigned long)p_memsz);
continue;
}
else {
segmentEnd = p_paddr + p_memsz - 1;
}
/* Check if segment start and end are both within any range */
bool startInRange =

View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
# test-range-overflow.py
#
# Regression test for the uint64_t overflow in squashelf's range filter
# (segmentEnd = p_paddr + p_memsz - 1). A crafted ELF64 PT_LOAD segment whose
# p_paddr + p_memsz wraps past 2^64 must NOT be smuggled past a range filter:
# its true span covers (almost) the whole address space, so it is out of range
# and must be excluded. Before the fix the wrapped end landed back inside the
# range and the segment was wrongly kept.
#
# Copyright (C) 2026 wolfSSL Inc.
#
# This file is part of wolfBoot.
#
# wolfBoot is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# wolfBoot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import os
import struct
import subprocess
import sys
import tempfile
EHSIZE = 64
PHSIZE = 56
UINT64_MAX = 0xFFFFFFFFFFFFFFFF
def make_elf64(path, paddr, memsz, filesz=0x10):
ph_off = EHSIZE
seg_off = ph_off + PHSIZE
ident = b"\x7fELF" + bytes([2, 1, 1, 0]) + b"\x00" * 8 # ELF64, little-endian
ehdr = ident + struct.pack(
"<HHIQQQIHHHHHH",
2, # type ET_EXEC
62, # machine x86-64
1, # version
0, # entry
ph_off, # ph_offset
0, # sh_offset
0, # flags
EHSIZE, # header_size
PHSIZE, # ph_entry_size
1, # ph_entry_count
0, 0, 0)
phdr = struct.pack(
"<IIQQQQQQ",
1, # PT_LOAD
5, # flags R+X
seg_off, # offset
paddr, # vaddr
paddr, # paddr
filesz, # file_size
memsz, # mem_size
0x1000) # align
with open(path, "wb") as f:
f.write(ehdr + phdr + b"\xAA" * filesz)
def run(squashelf, infile, outfile, rng):
return subprocess.run(
[squashelf, "-r", rng, infile, outfile],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode
def main():
squashelf = sys.argv[1] if len(sys.argv) > 1 else "./squashelf"
d = tempfile.mkdtemp()
rc = 0
# 1) Overflow segment: p_paddr + p_memsz - 1 wraps below p_paddr, so the
# wrapped end (~0x4fe) is inside [0, 0x1000] even though the real span
# covers the whole address space. It MUST be excluded (non-zero exit,
# no output segment).
bad = os.path.join(d, "overflow.elf")
make_elf64(bad, paddr=0x500, memsz=UINT64_MAX)
if run(squashelf, bad, os.path.join(d, "bad.out"), "0x0-0x1000") == 0:
print("FAIL: overflow segment was wrongly included by range filter")
rc = 1
else:
print("PASS: overflow segment excluded")
# 2) Regression guard: a normal in-range segment must still be kept.
good = os.path.join(d, "ok.elf")
make_elf64(good, paddr=0x500, memsz=0x100)
if run(squashelf, good, os.path.join(d, "good.out"), "0x0-0x1000") != 0:
print("FAIL: normal in-range segment was dropped")
rc = 1
else:
print("PASS: normal in-range segment kept")
sys.exit(rc)
if __name__ == "__main__":
main()