Skip to content

Commit c41e955

Browse files
committed
tasks/task_patch: bound BPS patch action-loop and size-prelude against attacker-controlled inputs
bps_apply_patch (tasks/task_patch.c) implemented BPS patch application without bounds checks on any of the per-command reads or writes. The action loop: while (bps.modify_offset < bps.modify_length - 12) { size_t _len = bps_decode(&bps); /* attacker-chosen */ unsigned mode = _len & 3; _len = (_len >> 2) + 1; switch (mode) { case TARGET_READ: while (_len--) { uint8_t data = bps_read(&bps); bps.target_data[bps.output_offset++] = data; /* OOB write */ } case SOURCE_READ: while (_len--) { uint8_t data = bps.source_data[bps.output_offset]; /* OOB read */ bps.target_data[bps.output_offset++] = data; } case SOURCE_COPY: bps.source_offset += offset; /* unbounded signed offset */ while (_len--) { uint8_t data = bps.source_data[bps.source_offset++]; /* OOB read */ bps.target_data[bps.output_offset++] = data; } case TARGET_COPY: bps.target_offset += offset; while (_len--) { uint8_t data = bps.target_data[bps.target_offset++]; bps.target_data[bps.output_offset++] = data; } } } A malicious .bps could: - Write attacker-chosen bytes far past the malloc'd target_data buffer (heap-buffer-overflow WRITE) via any of the four modes. - Read past source_data via SOURCE_COPY's unbounded signed offset. Negative offsets underflow source_offset to a huge size_t; positive offsets push it past source_length. The leaked bytes flow into target_data and the patch checksum, exposing heap contents. - Read past modify_data via TARGET_READ when _len exceeds the remaining patch bytes. The action-loop guard confirms only "at least one command's worth of bytes", not the full read range. The size prelude (modify_source_size / modify_target_size / modify_markup_size, decoded by bps_decode immediately after the "BPS1" header) was also unchecked: - modify_target_size declared as size_t silently truncated the uint64 bps_decode result on 32-bit hosts. A value of 0x100000000 wraps to 0; the subsequent malloc(0) returns a tiny allocation while bps.target_length is set to the raw uint64. The next target_data[output_offset++] write OOB by the truncated delta. - modify_markup_size was used as a loop bound consuming bytes from modify_data via bps_read. A value above the remaining patch bytes ran the read loop off the end of modify_data. Reachability: user has soft-patching enabled (the default when a .bps file with the same basename as the loaded ROM is present) and loads a ROM whose .bps was supplied by an attacker. Same threat class as CDFS / CHD / BSV file-load bugs. Fix: 1. Capture the three size-prelude values as raw uint64, reject anything above SIZE_MAX (so the size_t cast is guaranteed lossless), and reject markup_size that would drive bps_read past modify_length. 2. At the top of every action-loop iteration, bound _len against (target_length - output_offset). This single bound covers the target_data write in all four modes. 3. In each mode, bound the source-side read range: - SOURCE_READ: output_offset + _len <= source_length - TARGET_READ: modify_offset + _len <= modify_length - 12 - SOURCE_COPY: source_offset (post-offset-add) + _len <= source_length - TARGET_COPY: target_offset (post-offset-add) + _len <= target_length Adds samples/tasks/bps_patch/bps_patch_bounds_test as a regression test, registered in .github/workflows/Linux-samples-tasks.yml as an ASan-enabled step. Five cases covering OOB-write rejection, OOB-read rejection, legitimate operations, and the exact-capacity boundary. Test follows the existing samples/tasks pattern (verbatim copy of the post-fix predicates with maintenance-contract comment, ASan as the discriminator). Verified to fire under ASan when the bounds are removed: heap-buffer-overflow WRITE of size 1 at offset 4 of a 4-byte allocation in apply_action_loop. With the bounds the test passes clean.
1 parent 669468d commit c41e955

4 files changed

Lines changed: 541 additions & 3 deletions

File tree

.github/workflows/Linux-samples-tasks.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,26 @@ jobs:
171171
test -x bsv_replay_bounds_test
172172
timeout 60 ./bsv_replay_bounds_test
173173
echo "[pass] bsv_replay_bounds_test"
174+
175+
- name: Build and run bps_patch_bounds_test (ASan)
176+
shell: bash
177+
working-directory: samples/tasks/bps_patch
178+
run: |
179+
set -eu
180+
# Regression test for the .bps patch parser bounds in
181+
# tasks/task_patch.c::bps_apply_patch. Pre-fix a
182+
# malicious .bps could write attacker-chosen bytes
183+
# past the malloc'd target_data buffer (heap-buffer-
184+
# overflow WRITE), read past source_data (info leak),
185+
# and read past modify_data via TARGET_READ. The
186+
# size-prelude was also unbounded, allowing 32-bit
187+
# truncation of modify_target_size to drive a
188+
# smaller-than-expected allocation. Build under
189+
# AddressSanitizer so any reintroduction is caught
190+
# at the bounds level. If task_patch.c amends the
191+
# action-loop predicates, the verbatim copy in
192+
# bps_patch_bounds_test.c must follow.
193+
make clean all SANITIZER=address
194+
test -x bps_patch_bounds_test
195+
timeout 60 ./bps_patch_bounds_test
196+
echo "[pass] bps_patch_bounds_test"

samples/tasks/bps_patch/Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
TARGET := bps_patch_bounds_test
2+
3+
SOURCES := bps_patch_bounds_test.c
4+
5+
OBJS := $(SOURCES:.c=.o)
6+
7+
CFLAGS += -Wall -pedantic -std=gnu99 -g -O0
8+
9+
ifneq ($(SANITIZER),)
10+
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
11+
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
12+
endif
13+
14+
all: $(TARGET)
15+
16+
%.o: %.c
17+
$(CC) -c -o $@ $< $(CFLAGS)
18+
19+
$(TARGET): $(OBJS)
20+
$(CC) -o $@ $^ $(LDFLAGS)
21+
22+
clean:
23+
rm -f $(TARGET) $(OBJS)
24+
25+
.PHONY: clean

0 commit comments

Comments
 (0)