Skip to content

Commit d3d3893

Browse files
committed
fix(c): two CRITICAL memory bugs in standalone C (ASan/test-confirmed)
1. read_file() called realloc() directly on buf.data, which may point at the 4096-byte stack buffer (buffer_init uses stack storage when capacity<=4096). A file of exactly STACK_BUFFER_SIZE bytes -> realloc of a stack pointer -> heap corruption/crash. Now uses buffer_append(), which routes through buffer_grow() and correctly migrates stack->heap. 2. make_utf8() memcpy'd strlen(token) into a fixed uint8_t[4]; a runtime map (PRINTABLE_BINARY_MAP) with a >4-byte glyph token overflowed the stack. make_utf8 now clamps to MAX_UTF8_BYTES and load_map_from_path rejects over-long glyph tokens with a clear error. Also: encode_data() was missing buffer_prepare_return() that every sibling returner has (latent dangling-stack-return, masked only by INITIAL_BUFFER_SIZE > STACK_BUFFER_SIZE); added for correctness/consistency. Tests: test/test #5b (4095/4096/4097/8192-byte boundary roundtrip) and #5c (malformed runtime map rejected, not crashed). Both RED before, GREEN after; cross-impl-safe (Lua/Zig/C all pass).
1 parent 95aba31 commit d3d3893

2 files changed

Lines changed: 54 additions & 12 deletions

File tree

src/printable_binary.c

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ static void buffer_free(buffer_t *buf) {
273273
// Helper function to create UTF-8 sequence
274274
static utf8_sequence_t make_utf8(const char *bytes) {
275275
utf8_sequence_t seq = {0};
276-
seq.length = strlen(bytes);
277-
memcpy(seq.bytes, bytes, seq.length);
276+
size_t len = strlen(bytes);
277+
if (len > MAX_UTF8_BYTES) len = MAX_UTF8_BYTES; /* never overflow seq.bytes[4] */
278+
seq.length = len;
279+
memcpy(seq.bytes, bytes, len);
278280
return seq;
279281
}
280282

@@ -343,6 +345,13 @@ static bool load_map_from_path(const char *path) {
343345
}
344346
}
345347

348+
if (strlen(buffer) > MAX_UTF8_BYTES) {
349+
fprintf(stderr, "Error: character map '%s' glyph at index %d exceeds %d bytes\n",
350+
path, i, MAX_UTF8_BYTES);
351+
fclose(fp);
352+
exit(1);
353+
}
354+
346355
encode_table[i] = make_utf8(buffer);
347356
i++;
348357
}
@@ -952,6 +961,7 @@ static buffer_t encode_data(const uint8_t *input, size_t input_len, const option
952961
}
953962
}
954963

964+
buffer_prepare_return(&output);
955965
return output;
956966
}
957967

@@ -1115,16 +1125,12 @@ static buffer_t read_file(const char *filename) {
11151125
while (1) {
11161126
size_t bytes_read = fread(temp, 1, sizeof(temp), file);
11171127
if (bytes_read > 0) {
1118-
if (buf.size + bytes_read >= buf.capacity) {
1119-
buf.capacity = (buf.size + bytes_read) * 2;
1120-
buf.data = realloc(buf.data, buf.capacity);
1121-
if (!buf.data) {
1122-
fprintf(stderr, "Memory allocation failed\n");
1123-
exit(1);
1124-
}
1125-
}
1126-
memcpy(buf.data + buf.size, temp, bytes_read);
1127-
buf.size += bytes_read;
1128+
/* buffer_append routes through buffer_grow, which correctly migrates
1129+
* a stack-backed buffer to the heap. The previous hand-rolled realloc
1130+
* called realloc() directly on buf.data, corrupting/crashing when
1131+
* buf.data still pointed at the 4096-byte stack buffer (e.g. a file
1132+
* of exactly STACK_BUFFER_SIZE bytes). */
1133+
buffer_append(&buf, temp, bytes_read);
11281134
continue;
11291135
}
11301136

test/test

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,42 @@ else
217217
exit 1
218218
fi
219219
220+
# Test 5b: Buffer stack/heap boundary roundtrip
221+
# Regression: C buffer_init uses a 4096-byte stack buffer; read_file must not
222+
# realloc() that stack pointer. Exactly 4096 bytes is the crash boundary.
223+
echo -e "${BLUE}Test #5b: Buffer boundary roundtrip (4095/4096/4097/8192 bytes)${NC}"
224+
BOUNDARY_OK=1
225+
for sz in 4095 4096 4097 8192; do
226+
head -c "$sz" /dev/zero | tr '\0' 'Z' > "$TMP_BINARY"
227+
if ! $SCRIPT "$TMP_BINARY" > "$TMP_ENCODED" 2>/dev/null; then BOUNDARY_OK=0; echo "encode crashed at size $sz"; break; fi
228+
if ! $SCRIPT -d "$TMP_ENCODED" > "$TMP_DECODED" 2>/dev/null; then BOUNDARY_OK=0; echo "decode crashed at size $sz"; break; fi
229+
if ! cmp -s "$TMP_BINARY" "$TMP_DECODED"; then BOUNDARY_OK=0; echo "roundtrip mismatch at size $sz"; break; fi
230+
done
231+
if [[ "$BOUNDARY_OK" == "1" ]]; then
232+
echo -e "${GREEN}PASS${NC}"
233+
else
234+
echo -e "${RED}FAIL${NC}"
235+
exit 1
236+
fi
237+
238+
# Test 5c: Malformed runtime map (>4-byte glyph token) must be rejected, not crash
239+
# Regression: C make_utf8 memcpy'd strlen(token) into a 4-byte array (stack smash).
240+
echo -e "${BLUE}Test #5c: Malformed runtime map rejected gracefully${NC}"
241+
if [[ -f character_map.txt ]]; then
242+
BADMAP=$(mktemp)
243+
awk 'BEGIN{done=0} /^##/{print;next} (done==0){print "TOOLONG8"; done=1; next} {print}' character_map.txt > "$BADMAP"
244+
rc=0
245+
PRINTABLE_BINARY_MAP="$BADMAP" $SCRIPT <<< "x" >/dev/null 2>&1 || rc=$?
246+
rm -f "$BADMAP"
247+
if [[ $rc -ge 128 ]]; then
248+
echo -e "${RED}FAIL${NC} (crashed with signal, exit $rc)"
249+
exit 1
250+
fi
251+
echo -e "${GREEN}PASS${NC}"
252+
else
253+
echo -e "${YELLOW}SKIP (no character_map.txt in cwd)${NC}"
254+
fi
255+
220256
# Test 6: Piped input
221257
echo -e "${BLUE}Test #6: Piped input${NC}"
222258
RESULT=$(echo -n "Test" | $SCRIPT | $SCRIPT -d)

0 commit comments

Comments
 (0)