mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 17:28:49 -10:00
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.
This commit is contained in:
1 parent
669468db2e
commit
c41e955464
4 files changed
+541
-3
No files matched your search
@@ -171,3 +171,26 @@ jobs:
|
||||
test -x bsv_replay_bounds_test
|
||||
timeout 60 ./bsv_replay_bounds_test
|
||||
echo "[pass] bsv_replay_bounds_test"
|
||||
|
||||
- name: Build and run bps_patch_bounds_test (ASan)
|
||||
shell: bash
|
||||
working-directory: samples/tasks/bps_patch
|
||||
run: |
|
||||
set -eu
|
||||
# Regression test for the .bps patch parser bounds in
|
||||
# tasks/task_patch.c::bps_apply_patch. Pre-fix a
|
||||
# malicious .bps could write attacker-chosen bytes
|
||||
# past the malloc'd target_data buffer (heap-buffer-
|
||||
# overflow WRITE), read past source_data (info leak),
|
||||
# and read past modify_data via TARGET_READ. The
|
||||
# size-prelude was also unbounded, allowing 32-bit
|
||||
# truncation of modify_target_size to drive a
|
||||
# smaller-than-expected allocation. Build under
|
||||
# AddressSanitizer so any reintroduction is caught
|
||||
# at the bounds level. If task_patch.c amends the
|
||||
# action-loop predicates, the verbatim copy in
|
||||
# bps_patch_bounds_test.c must follow.
|
||||
make clean all SANITIZER=address
|
||||
test -x bps_patch_bounds_test
|
||||
timeout 60 ./bps_patch_bounds_test
|
||||
echo "[pass] bps_patch_bounds_test"
|
||||
@@ -0,0 +1,25 @@
|
||||
TARGET := bps_patch_bounds_test
|
||||
|
||||
SOURCES := bps_patch_bounds_test.c
|
||||
|
||||
OBJS := $(SOURCES:.c=.o)
|
||||
|
||||
CFLAGS += -Wall -pedantic -std=gnu99 -g -O0
|
||||
|
||||
ifneq ($(SANITIZER),)
|
||||
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
|
||||
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
|
||||
endif
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) -c -o $@ $< $(CFLAGS)
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) $(OBJS)
|
||||
|
||||
.PHONY: clean
|
||||
@@ -0,0 +1,415 @@
|
||||
/* Copyright (C) 2010-2026 The RetroArch team
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* The following license statement only applies to this file (bps_patch_bounds_test.c).
|
||||
* ---------------------------------------------------------------------------------------
|
||||
*
|
||||
* Permission is hereby granted, free of charge,
|
||||
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* Regression test for the .bps patch parser bounds in
|
||||
* tasks/task_patch.c::bps_apply_patch.
|
||||
*
|
||||
* The .bps format is a variable-length-integer-driven
|
||||
* stream of action commands operating on three buffers
|
||||
* (modify, source, target). Pre-this-patch the action
|
||||
* loop performed every read/write without bounds:
|
||||
*
|
||||
* case TARGET_READ:
|
||||
* while (_len--) {
|
||||
* uint8_t data = bps_read(&bps);
|
||||
* bps.target_data[bps.output_offset++] = data; // OOB write
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* 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; // OOB write
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* A malicious .bps patch could write attacker-chosen bytes
|
||||
* past the malloc'd target_data buffer (heap-buffer-overflow
|
||||
* WRITE), read past source_data (heap-info leak that flows
|
||||
* into target_data and the patch checksum, exposing heap
|
||||
* contents to a process that reads the patched output), and
|
||||
* read past modify_data via TARGET_READ when the per-command
|
||||
* _len exceeded the remaining bytes.
|
||||
*
|
||||
* Plus the size-prelude: the three variable-length integers
|
||||
* decoded immediately after the "BPS1" header (modify_source_
|
||||
* size, modify_target_size, modify_markup_size) had no upper
|
||||
* bound. modify_target_size silently truncated to size_t on
|
||||
* 32-bit hosts producing a smaller allocation than
|
||||
* bps.target_length expected; modify_markup_size of UINT64_MAX
|
||||
* drove the markup-skip loop into reading the entire patch
|
||||
* buffer and beyond.
|
||||
*
|
||||
* Reachability: user has soft-patching enabled, places a
|
||||
* malicious .bps next to a ROM (or downloads a "ROM pack"
|
||||
* containing patches). Same threat class as CDFS / CHD / BSV
|
||||
* file-load bugs.
|
||||
*
|
||||
* Fix: bound _len against (target_length - output_offset)
|
||||
* once at the top of each loop iteration; bound source/target
|
||||
* offset+_len against the respective buffers; bound the size
|
||||
* prelude against SIZE_MAX and remaining patch bytes.
|
||||
*
|
||||
* IMPORTANT: this test keeps verbatim copies of the post-fix
|
||||
* predicates from tasks/task_patch.c. If task_patch.c amends,
|
||||
* the copies below must follow. Convention used by
|
||||
* archive_name_safety_test, http_method_match_test,
|
||||
* video_shader_wildcard_test, input_remap_bounds_test,
|
||||
* bsv_replay_bounds_test in this directory tree.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
|
||||
enum bps_mode_t { SOURCE_READ = 0, TARGET_READ, SOURCE_COPY, TARGET_COPY };
|
||||
|
||||
struct mock_bps {
|
||||
const uint8_t *modify_data;
|
||||
const uint8_t *source_data;
|
||||
uint8_t *target_data;
|
||||
uint64_t modify_length;
|
||||
uint64_t source_length;
|
||||
uint64_t target_length;
|
||||
uint64_t modify_offset;
|
||||
uint64_t source_offset;
|
||||
uint64_t target_offset;
|
||||
uint64_t output_offset;
|
||||
};
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
static uint8_t bps_read(struct mock_bps *b)
|
||||
{
|
||||
/* Test scope: assume caller has bounded the read. If a
|
||||
* test would have caused this to OOB, the post-fix
|
||||
* predicate should have rejected the patch first. */
|
||||
return b->modify_data[b->modify_offset++];
|
||||
}
|
||||
|
||||
static uint64_t bps_decode(struct mock_bps *b)
|
||||
{
|
||||
uint64_t data = 0, shift = 1;
|
||||
for (;;)
|
||||
{
|
||||
uint8_t x = bps_read(b);
|
||||
data += (x & 0x7f) * shift;
|
||||
if (x & 0x80) break;
|
||||
shift <<= 7;
|
||||
data += shift;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/* === verbatim copy of the post-fix action loop predicates
|
||||
* from tasks/task_patch.c::bps_apply_patch. Returns 0
|
||||
* on success, non-zero on a bounds-rejection (the
|
||||
* production code returns specific patch_error values;
|
||||
* this mock returns generic non-zero). If task_patch.c
|
||||
* amends the predicates, this copy must follow. === */
|
||||
static int apply_action_loop(struct mock_bps *bps)
|
||||
{
|
||||
while (bps->modify_offset < bps->modify_length - 12)
|
||||
{
|
||||
uint64_t _len = bps_decode(bps);
|
||||
unsigned mode = _len & 3;
|
||||
_len = (_len >> 2) + 1;
|
||||
|
||||
if ( bps->output_offset >= bps->target_length
|
||||
|| _len > bps->target_length - bps->output_offset)
|
||||
return 1; /* PATCH_TARGET_INVALID */
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case SOURCE_READ:
|
||||
if (bps->output_offset + _len > bps->source_length)
|
||||
return 2; /* PATCH_SOURCE_INVALID */
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps->source_data[bps->output_offset];
|
||||
bps->target_data[bps->output_offset++] = data;
|
||||
}
|
||||
break;
|
||||
|
||||
case TARGET_READ:
|
||||
if (bps->modify_offset + _len > bps->modify_length - 12)
|
||||
return 3; /* PATCH_PATCH_INVALID */
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps_read(bps);
|
||||
bps->target_data[bps->output_offset++] = data;
|
||||
}
|
||||
break;
|
||||
|
||||
case SOURCE_COPY:
|
||||
case TARGET_COPY:
|
||||
{
|
||||
int offset = (int)bps_decode(bps);
|
||||
bool negative = offset & 1;
|
||||
offset >>= 1;
|
||||
if (negative)
|
||||
offset = -offset;
|
||||
|
||||
if (mode == SOURCE_COPY)
|
||||
{
|
||||
bps->source_offset += offset;
|
||||
if ( bps->source_offset > bps->source_length
|
||||
|| _len > bps->source_length - bps->source_offset)
|
||||
return 2;
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps->source_data[bps->source_offset++];
|
||||
bps->target_data[bps->output_offset++] = data;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bps->target_offset += offset;
|
||||
if ( bps->target_offset > bps->target_length
|
||||
|| _len > bps->target_length - bps->target_offset)
|
||||
return 1;
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps->target_data[bps->target_offset++];
|
||||
bps->target_data[bps->output_offset++] = data;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* === end verbatim copy === */
|
||||
|
||||
/* Helper: run the post-fix predicates against a patch+source+
|
||||
* target setup, with target_data heap-allocated to exactly
|
||||
* the legitimate target_length so ASan flags any OOB write. */
|
||||
static int run_apply(const uint8_t *patch, size_t patch_len,
|
||||
const uint8_t *src, size_t src_len, size_t target_len)
|
||||
{
|
||||
struct mock_bps bps;
|
||||
uint8_t *target;
|
||||
int rv;
|
||||
|
||||
if (!(target = (uint8_t*)malloc(target_len ? target_len : 1)))
|
||||
return -1;
|
||||
memset(target, 0, target_len ? target_len : 1);
|
||||
|
||||
memset(&bps, 0, sizeof(bps));
|
||||
bps.modify_data = patch;
|
||||
bps.source_data = src;
|
||||
bps.target_data = target;
|
||||
bps.modify_length = patch_len;
|
||||
bps.source_length = src_len;
|
||||
bps.target_length = target_len;
|
||||
/* Skip the "BPS1" header + size prelude. The test
|
||||
* harness sets modify_offset directly; in production the
|
||||
* same offset is reached by the header read and three
|
||||
* bps_decode calls before the action loop. */
|
||||
bps.modify_offset = 7; /* "BPS1" + 0x80, 0x80, 0x80 (zero sizes) */
|
||||
|
||||
rv = apply_action_loop(&bps);
|
||||
|
||||
free(target);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/* Build a minimal valid header + 3 zero size declarations
|
||||
* starting at out, returning the bytes written. */
|
||||
static size_t put_header(uint8_t *out)
|
||||
{
|
||||
out[0] = 'B'; out[1] = 'P'; out[2] = 'S'; out[3] = '1';
|
||||
out[4] = 0x80; /* source size = 0 (single-byte: high bit set, value 0) */
|
||||
out[5] = 0x80; /* target size = 0 */
|
||||
out[6] = 0x80; /* markup size = 0 */
|
||||
return 7;
|
||||
}
|
||||
|
||||
/* ---- tests ------------------------------------------------ */
|
||||
|
||||
static void test_target_read_oob_write_rejected(void)
|
||||
{
|
||||
/* Craft: TARGET_READ command with _len = 32 (encoded as 0xFD).
|
||||
* Target buffer is 4 bytes. Pre-fix: 32 bytes written into a
|
||||
* 4-byte allocation (heap-buffer-overflow WRITE). Post-fix:
|
||||
* the top-of-loop bound rejects with PATCH_TARGET_INVALID. */
|
||||
uint8_t patch[64];
|
||||
size_t off = put_header(patch);
|
||||
int i, rv;
|
||||
patch[off++] = 0xFD; /* mode=1 (TARGET_READ), _len = (125>>2)+1 = 32 */
|
||||
for (i = 0; i < 32; i++)
|
||||
patch[off++] = (uint8_t)(0xAA + i);
|
||||
for (i = 0; i < 12; i++)
|
||||
patch[off++] = 0; /* trailing checksums (12 bytes) */
|
||||
|
||||
rv = run_apply(patch, off, NULL, 0, 4);
|
||||
if (rv == 0)
|
||||
{
|
||||
printf("[ERROR] TARGET_READ with _len=32 into target=4 was accepted\n");
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] TARGET_READ over-length rejected without OOB write\n");
|
||||
}
|
||||
|
||||
static void test_source_copy_oob_read_rejected(void)
|
||||
{
|
||||
/* SOURCE_COPY with offset = 100 against a source of 8 bytes.
|
||||
* After the offset, source_offset = 100 > source_length = 8
|
||||
* which the new bound rejects. Pre-fix this would have run
|
||||
* source_data[100++] for _len iterations -- heap-buffer-
|
||||
* overflow READ. */
|
||||
uint8_t patch[64], src[8] = {1,2,3,4,5,6,7,8};
|
||||
size_t off = put_header(patch);
|
||||
int rv, i;
|
||||
/* Encode: mode=SOURCE_COPY (2), _len=4.
|
||||
* stored_len = ((4-1) << 2) | 2 = 14 = 0x0E.
|
||||
* High bit not set, so it's not a single-byte encoding.
|
||||
* For the test we want a single-byte command so just use
|
||||
* 0x8E (14 + 0x80). bps_decode: x=0x8E, x&0x7F=0x0E, data=14,
|
||||
* x&0x80=>break. So _len=4, mode=2. */
|
||||
patch[off++] = 0x8E;
|
||||
/* offset = 200 (positive, so encoded value = 200<<1 = 400).
|
||||
* 400 > 127 so multi-byte: low 7 bits = 400 & 0x7f = 16 (0x10),
|
||||
* second byte = (400>>7)+something... easier to use a small
|
||||
* positive offset directly. offset = 100 -> encoded = 200.
|
||||
* 200 > 127 so 2-byte encoding:
|
||||
* first byte: 200 & 0x7F = 72 = 0x48 (high bit clear)
|
||||
* shift <<= 7 (=128), data += 128 => data = 72 + 128 = 200
|
||||
* ... wait that's the decoder, not the encoder.
|
||||
* For decoder result 200:
|
||||
* byte 1: x1 (high bit clear), data += x1 * 1 = x1, then shift=128, data += 128
|
||||
* byte 2: x2 (high bit set), data += (x2 & 0x7F) * 128
|
||||
* want data = 200
|
||||
* 200 = x1 + 128 + (x2 & 0x7F) * 128
|
||||
* = x1 + 128 * (1 + (x2 & 0x7F))
|
||||
* if x2 & 0x7F = 0, then 200 = x1 + 128, x1 = 72.
|
||||
* So bytes: 0x48, 0x80 (x1=0x48, x2=0x80). Verify:
|
||||
* data=0, shift=1; read 0x48, data += 0x48*1 = 72, no high bit, shift<<=7 (=128), data += 128 = 200, loop;
|
||||
* read 0x80, data += 0 * 128 = 200, high bit => break. ✓ */
|
||||
patch[off++] = 0x48;
|
||||
patch[off++] = 0x80;
|
||||
/* trailing 12 bytes */
|
||||
for (i = 0; i < 12; i++)
|
||||
patch[off++] = 0;
|
||||
|
||||
rv = run_apply(patch, off, src, sizeof(src), 256);
|
||||
if (rv == 0)
|
||||
{
|
||||
printf("[ERROR] SOURCE_COPY with offset=100 vs src=8 was accepted\n");
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] SOURCE_COPY over-length rejected without OOB read\n");
|
||||
}
|
||||
|
||||
static void test_legitimate_target_read_succeeds(void)
|
||||
{
|
||||
/* TARGET_READ with _len=4 into target=4. */
|
||||
uint8_t patch[64];
|
||||
size_t off = put_header(patch);
|
||||
int rv, i;
|
||||
/* _len=4: stored = (3<<2)|1 = 13. Single byte 0x8D. */
|
||||
patch[off++] = 0x8D;
|
||||
patch[off++] = 'A'; patch[off++] = 'B';
|
||||
patch[off++] = 'C'; patch[off++] = 'D';
|
||||
for (i = 0; i < 12; i++)
|
||||
patch[off++] = 0;
|
||||
|
||||
rv = run_apply(patch, off, NULL, 0, 4);
|
||||
if (rv != 0)
|
||||
{
|
||||
printf("[ERROR] legitimate TARGET_READ rv=%d\n", rv);
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] legitimate TARGET_READ accepted\n");
|
||||
}
|
||||
|
||||
static void test_legitimate_source_copy_succeeds(void)
|
||||
{
|
||||
uint8_t patch[64], src[8] = {1,2,3,4,5,6,7,8};
|
||||
size_t off = put_header(patch);
|
||||
int rv, i;
|
||||
/* SOURCE_COPY mode=2, _len=4 -> stored = (3<<2)|2 = 14, byte 0x8E */
|
||||
patch[off++] = 0x8E;
|
||||
/* offset = 0: encoded value = 0, single-byte 0x80 */
|
||||
patch[off++] = 0x80;
|
||||
for (i = 0; i < 12; i++)
|
||||
patch[off++] = 0;
|
||||
|
||||
rv = run_apply(patch, off, src, sizeof(src), 8);
|
||||
if (rv != 0)
|
||||
{
|
||||
printf("[ERROR] legitimate SOURCE_COPY rv=%d\n", rv);
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] legitimate SOURCE_COPY accepted\n");
|
||||
}
|
||||
|
||||
static void test_target_read_at_exact_capacity(void)
|
||||
{
|
||||
/* TARGET_READ with _len exactly equal to target_length.
|
||||
* Should succeed (boundary). */
|
||||
uint8_t patch[64];
|
||||
size_t off = put_header(patch);
|
||||
int rv, i;
|
||||
/* _len=8: stored=(7<<2)|1=29, byte 0x9D */
|
||||
patch[off++] = 0x9D;
|
||||
for (i = 0; i < 8; i++)
|
||||
patch[off++] = 'X';
|
||||
for (i = 0; i < 12; i++)
|
||||
patch[off++] = 0;
|
||||
|
||||
rv = run_apply(patch, off, NULL, 0, 8);
|
||||
if (rv != 0)
|
||||
{
|
||||
printf("[ERROR] boundary TARGET_READ (_len=target_length=8) rejected, rv=%d\n", rv);
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] boundary TARGET_READ (_len=target_length) accepted\n");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_target_read_oob_write_rejected();
|
||||
test_source_copy_oob_read_rejected();
|
||||
test_legitimate_target_read_succeeds();
|
||||
test_legitimate_source_copy_succeeds();
|
||||
test_target_read_at_exact_capacity();
|
||||
|
||||
if (failures)
|
||||
{
|
||||
printf("\n%d bps_patch_bounds test(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("\nAll bps_patch_bounds regression tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
+78
-3
@@ -168,9 +168,38 @@ static enum patch_error bps_apply_patch(
|
||||
|| (bps_read(&bps) != '1'))
|
||||
return PATCH_PATCH_INVALID_HEADER;
|
||||
|
||||
modify_source_size = bps_decode(&bps);
|
||||
modify_target_size = bps_decode(&bps);
|
||||
modify_markup_size = bps_decode(&bps);
|
||||
{
|
||||
uint64_t raw_source = bps_decode(&bps);
|
||||
uint64_t raw_target = bps_decode(&bps);
|
||||
uint64_t raw_markup = bps_decode(&bps);
|
||||
|
||||
/* All three sizes are decoded from attacker-controlled
|
||||
* variable-length integers in the patch file with no
|
||||
* inherent upper bound (each can reach UINT64_MAX).
|
||||
* Three separate sanity checks are required before they
|
||||
* are assigned into size_t locals, where (on 32-bit
|
||||
* targets) values above SIZE_MAX would silently truncate:
|
||||
* - markup_size is consumed via bps_read below; if it
|
||||
* exceeds the remaining bytes in modify_data, those
|
||||
* reads run off the end of the patch buffer.
|
||||
* - target_size on a 32-bit host would truncate at the
|
||||
* assignment, producing a smaller-than-expected
|
||||
* allocation while bps.target_length stays at the raw
|
||||
* uint64. Subsequent target_data[output_offset++]
|
||||
* writes OOB by the truncated delta. Reject anything
|
||||
* that won't fit in size_t outright; malloc would have
|
||||
* failed anyway. */
|
||||
if (raw_markup > modify_length - bps.modify_offset)
|
||||
return PATCH_PATCH_INVALID;
|
||||
if (raw_target > (uint64_t)SIZE_MAX)
|
||||
return PATCH_TARGET_ALLOC_FAILED;
|
||||
if (raw_source > (uint64_t)SIZE_MAX)
|
||||
return PATCH_SOURCE_TOO_SMALL;
|
||||
|
||||
modify_source_size = (size_t)raw_source;
|
||||
modify_target_size = (size_t)raw_target;
|
||||
modify_markup_size = (size_t)raw_markup;
|
||||
}
|
||||
|
||||
for (i = 0; i < modify_markup_size; i++)
|
||||
bps_read(&bps);
|
||||
@@ -198,9 +227,27 @@ static enum patch_error bps_apply_patch(
|
||||
|
||||
_len = (_len >> 2) + 1;
|
||||
|
||||
/* Every action mode below performs target_data[output_
|
||||
* offset++] = ... up to _len times. Pre-this-patch
|
||||
* none of the modes checked output_offset against
|
||||
* bps.target_length, so a malicious .bps could write
|
||||
* arbitrary attacker-chosen bytes past the malloc'd
|
||||
* target_data buffer (heap-buffer-overflow WRITE).
|
||||
* Bound _len against the remaining target capacity
|
||||
* once at the top so each mode below stays in
|
||||
* bounds. */
|
||||
if ( bps.output_offset >= bps.target_length
|
||||
|| _len > bps.target_length - bps.output_offset)
|
||||
return PATCH_TARGET_INVALID;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case SOURCE_READ:
|
||||
/* SOURCE_READ reads from source_data[output_offset]
|
||||
* for the same output_offset range, so the same
|
||||
* bound applies to source_data too. */
|
||||
if (bps.output_offset + _len > bps.source_length)
|
||||
return PATCH_SOURCE_INVALID;
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps.source_data[bps.output_offset];
|
||||
@@ -210,6 +257,14 @@ static enum patch_error bps_apply_patch(
|
||||
break;
|
||||
|
||||
case TARGET_READ:
|
||||
/* TARGET_READ reads _len bytes from modify_data via
|
||||
* bps_read. The action loop's outer guard only
|
||||
* confirms there is at least one command's worth
|
||||
* of bytes (modify_offset < modify_length - 12);
|
||||
* a long _len plus the 12-byte trailer can read
|
||||
* past modify_length. */
|
||||
if (bps.modify_offset + _len > bps.modify_length - 12)
|
||||
return PATCH_PATCH_INVALID;
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps_read(&bps);
|
||||
@@ -232,6 +287,19 @@ static enum patch_error bps_apply_patch(
|
||||
if (mode == SOURCE_COPY)
|
||||
{
|
||||
bps.source_offset += offset;
|
||||
/* Validate the resulting source_offset and the
|
||||
* full read range against source_length. Pre-
|
||||
* this-patch a malicious offset could push
|
||||
* source_offset past source_length (or, with a
|
||||
* negative offset, underflow it to a huge size_t),
|
||||
* driving source_data[source_offset++] reads
|
||||
* arbitrarily far OOB. An attacker who can read
|
||||
* past source_data into adjacent heap leaks heap
|
||||
* contents into target_data and the checksum
|
||||
* calculation. */
|
||||
if ( bps.source_offset > bps.source_length
|
||||
|| _len > bps.source_length - bps.source_offset)
|
||||
return PATCH_SOURCE_INVALID;
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps.source_data[bps.source_offset++];
|
||||
@@ -242,6 +310,13 @@ static enum patch_error bps_apply_patch(
|
||||
else
|
||||
{
|
||||
bps.target_offset += offset;
|
||||
/* TARGET_COPY both reads from and writes to
|
||||
* target_data; bound the read pointer the same
|
||||
* way as SOURCE_COPY above. output_offset is
|
||||
* already bounded by the top-of-loop check. */
|
||||
if ( bps.target_offset > bps.target_length
|
||||
|| _len > bps.target_length - bps.target_offset)
|
||||
return PATCH_TARGET_INVALID;
|
||||
while (_len--)
|
||||
{
|
||||
uint8_t data = bps.target_data[bps.target_offset++];
|
||||
|
||||
Reference in new issue
Block a user