diff --git a/.gitignore b/.gitignore index 539fdcd2..c215a1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -207,6 +207,7 @@ tools/squashelf/** !tools/squashelf/Makefile !tools/squashelf/README.md !tools/squashelf/test-range-overflow.py +!tools/squashelf/test-align-overflow.py # Generated configuration files .config diff --git a/tools/squashelf/Makefile b/tools/squashelf/Makefile index cb6ec5b5..f7842c6f 100644 --- a/tools/squashelf/Makefile +++ b/tools/squashelf/Makefile @@ -27,6 +27,7 @@ debug: all test: $(TARGET) python3 test-range-overflow.py ./$(TARGET) + python3 test-align-overflow.py ./$(TARGET) $(TARGET): $(TARGET).o @echo "Building squashelf tool" diff --git a/tools/squashelf/squashelf.c b/tools/squashelf/squashelf.c index cffb2e12..c8f676d8 100644 --- a/tools/squashelf/squashelf.c +++ b/tools/squashelf/squashelf.c @@ -752,10 +752,31 @@ int main(int argCount, char** argValues) /* Align the segment according to its alignment requirement if * needed */ if (p_align > 1) { + /* Guard against overflow in the round-up (CWE-190). p_align + * comes straight from a possibly crafted program header; a + * large value would wrap current_offset to a small value and + * place this segment's data over the ELF header. */ + if (current_offset > UINT64_MAX - (p_align - 1)) { + fprintf(stderr, + "Segment %zu alignment 0x%lx overflows file " + "offset 0x%lx\n", + i, (unsigned long)p_align, + (unsigned long)current_offset); + goto cleanup; + } current_offset = (current_offset + p_align - 1) & ~(p_align - 1); } + /* The 32-bit program header offset is a uint32_t; a larger value + * would be silently truncated and corrupt the output layout. */ + if (current_offset > UINT32_MAX) { + fprintf(stderr, + "Segment %zu offset 0x%lx exceeds 32-bit ELF range\n", + i, (unsigned long)current_offset); + goto cleanup; + } + /* Update the segment's offset */ ph32_array[i].offset = current_offset; p_filesz = ph32_array[i].file_size; @@ -770,6 +791,18 @@ int main(int argCount, char** argValues) /* Align the segment according to its alignment requirement if * needed */ if (p_align > 1) { + /* Guard against overflow in the round-up (CWE-190). p_align + * comes straight from a possibly crafted program header; a + * value near UINT64_MAX would wrap current_offset to a small + * value and place this segment's data over the ELF header. */ + if (current_offset > UINT64_MAX - (p_align - 1)) { + fprintf(stderr, + "Segment %zu alignment 0x%lx overflows file " + "offset 0x%lx\n", + i, (unsigned long)p_align, + (unsigned long)current_offset); + goto cleanup; + } current_offset = (current_offset + p_align - 1) & ~(p_align - 1); } diff --git a/tools/squashelf/test-align-overflow.py b/tools/squashelf/test-align-overflow.py new file mode 100644 index 00000000..d7386c40 --- /dev/null +++ b/tools/squashelf/test-align-overflow.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# test-align-overflow.py +# +# Regression test for the integer overflow in squashelf's segment-offset +# alignment round-up (current_offset = (current_offset + p_align - 1) & +# ~(p_align - 1)). p_align comes straight from a possibly crafted program +# header. A value near UINT64_MAX (ELF64) wraps the sum to a tiny value, and a +# value that rounds the offset past 2^32 (ELF32) is silently truncated to a +# uint32_t. Either way the segment data lands at a wrong (often zero) file +# offset, clobbering the ELF header while squashelf still reports success. +# After the fix squashelf must reject such inputs with a non-zero exit instead +# of writing a corrupt output ELF. +# +# 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 + +EHSIZE64 = 64 +PHSIZE64 = 56 +EHSIZE32 = 52 +PHSIZE32 = 32 +UINT64_MAX = 0xFFFFFFFFFFFFFFFF +UINT32_MAX = 0xFFFFFFFF + + +def make_elf64(path, align, filesz=0x10): + ph_off = EHSIZE64 + seg_off = ph_off + PHSIZE64 + ident = b"\x7fELF" + bytes([2, 1, 1, 0]) + b"\x00" * 8 # ELF64, little-endian + ehdr = ident + struct.pack( + " 1 else "./squashelf" + rc = 0 + with tempfile.TemporaryDirectory() as d: + # 1) ELF64 alignment near UINT64_MAX wraps the round-up to a tiny + # offset. squashelf MUST reject it (non-zero exit), not write a + # corrupt ELF over its own header and report success. + bad64 = os.path.join(d, "align64.elf") + out64 = os.path.join(d, "align64.out") + make_elf64(bad64, align=UINT64_MAX) + if run(squashelf, bad64, out64) == 0: + print("FAIL: ELF64 overflowing alignment was accepted") + rc = 1 + else: + print("PASS: ELF64 overflowing alignment rejected") + + # 2) ELF32 alignment that rounds the offset past 2^32; the uint32_t + # offset field would silently truncate (to 0 here). Must be rejected. + bad32 = os.path.join(d, "align32.elf") + out32 = os.path.join(d, "align32.out") + make_elf32(bad32, align=UINT32_MAX) + if run(squashelf, bad32, out32) == 0: + print("FAIL: ELF32 truncating alignment was accepted") + rc = 1 + else: + print("PASS: ELF32 truncating alignment rejected") + + # 3) Regression guard: normal page-aligned segments must still succeed. + ok64 = os.path.join(d, "ok64.elf") + ok64o = os.path.join(d, "ok64.out") + make_elf64(ok64, align=0x1000) + if run(squashelf, ok64, ok64o) != 0: + print("FAIL: normal ELF64 segment was rejected") + rc = 1 + else: + print("PASS: normal ELF64 segment kept") + + ok32 = os.path.join(d, "ok32.elf") + ok32o = os.path.join(d, "ok32.out") + make_elf32(ok32, align=0x1000) + if run(squashelf, ok32, ok32o) != 0: + print("FAIL: normal ELF32 segment was rejected") + rc = 1 + else: + print("PASS: normal ELF32 segment kept") + + sys.exit(rc) + + +if __name__ == "__main__": + main()