Skip to content

Commit 4c1322c

Browse files
derfsssclaude
andauthored
fix(altivec): vec_strcpy reads past source page boundary on G4 (#438)
vec_strcpy guarded its speculative source loads against crossing a 4K page using the DESTINATION pointer, but the lvx loads come from the SOURCE. When the source string ends near a page boundary while the destination is mid-page, the dest-based counter is too large and the loop keeps loading source vectors into the next (possibly unmapped) page, causing a DSI crash on AltiVec CPUs (G4 / MPC744x/745x). Guard from the QW-aligned source instead (lvx loads use addr & ~15), so the loop stops at the source's page boundary, finds the NUL in the last in-page vector, and finishes the tail by bytes. AltiVec acceleration is preserved. Adds test_programs/memory/{altivec_guard,memcpy_guard}.c -- guard-page over-read/over-write tests for the AltiVec string/mem routines. Verified under QEMU emulating an MPC 7447/7457 (G4, AltiVec): altivec_guard goes from a DSI crash at strcpy len=16 to PASS across bcopy/memcmp/strcpy/ bzero x len 1..1024. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 729ab37 commit 4c1322c

3 files changed

Lines changed: 311 additions & 2 deletions

File tree

library/cpu/altivec/vec_strcpy.sx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ vec_strcpy:
145145
mr DMS,SMD // IU1 |dst - src| = src - dst
146146
Pos_value:
147147
subf. QBC,DST,ADD // IU1 Bytes to even QW start of vect (min 32)
148-
addi ADD,DD,PAGE_SIZE // IU1 dst addr in next 4K page
148+
addi ADD,SRC,PAGE_SIZE // IU1 SRC addr in next 4K page (loads fault on
149+
// the SOURCE, so guard the source's page,
150+
// not the dest's -- else an unmapped page
151+
// just past src faults while dst is mid-page)
149152
cmpi cr7,0,DMS,MIN_VEC // IU1 Check for min byte count separation
150153

151154
mtctr QBC // IU2 Init counter
@@ -171,7 +174,12 @@ v_strcpy:
171174
#ifdef VRSAVE
172175
mfspr RSV,VRSV // IU2 Get current VRSAVE contents
173176
#endif
174-
subf. PBC,DD,ADD // IU1 Now bytes to next 4K page
177+
rlwinm Rt,DS,0,0,27 // IU1 Align src down to QW first: lvx loads are
178+
// 16-aligned (use DS&~15), so an unaligned
179+
// (rg-DS)/16 rounds to 0 and New_page_0 then
180+
// loads a whole page past the src page end
181+
subf. PBC,Rt,ADD // IU1 QW-exact bytes from aligned src to its 4K
182+
// page boundary (loads fault on SRC, not dest)
175183

176184
#ifdef VRSAVE
177185
oris Rt,RSV,0xff00 // IU1 Or in registers used by this routine
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* altivec_guard.c -- expose tail over-read / over-write in clib4's AltiVec
3+
* string/mem routines (the ones actually selected on a G4).
4+
*
5+
* clib4.c (libOpen) routes, on an AltiVec CPU:
6+
* strcpy -> vec_strcpy memcmp -> vec_memcmp
7+
* bzero -> vec_bzero bcopy -> vec_bcopy
8+
* (memcpy/memmove are NOT AltiVec-routed, which is why a memcpy-only test
9+
* passes -- see memcpy_guard.c.) These hand-written routines
10+
* (library/cpu/altivec/*.sx) use the classic "load two vectors + vperm"
11+
* unaligned idiom, which can touch up to 15 bytes past the end of a buffer.
12+
* Harmless mid-heap, but it FAULTS (DSI) when the buffer ends just before an
13+
* unmapped page -- e.g. a class-name UTF-8 string compared during VM class
14+
* loading, which crashed JamVM on the QEMU 'amigaone' (G4) target.
15+
*
16+
* This test puts the READ buffer (bcopy/memcmp/strcpy source) and the WRITE
17+
* buffer (bzero dest) so they end EXACTLY at a PROT_NONE guard page, so any
18+
* access one byte past the requested length faults. A SIGSEGV handler +
19+
* siglongjmp lets the sweep continue and name the offending routine+length.
20+
* If clib4 does not deliver SIGSEGV, the process hard-crashes at the first
21+
* offending case (Grim Reaper) -- itself the proof; the stderr breadcrumb
22+
* (printed every 64 lengths) narrows it down.
23+
*
24+
* Only meaningful on AltiVec CPUs (G4 and the QEMU 'amigaone' machine). On
25+
* non-AltiVec CPUs (G3, 440/460, X5000/P5020, A1222/P1022) clib4 uses scalar
26+
* routines and this should PASS.
27+
*
28+
* Build: drop in test_programs/memory/; `make compile-tests` builds it with
29+
* -mcrt=clib4 -fno-builtin. PASS == rc 0.
30+
*/
31+
#include <stdio.h>
32+
#include <stdlib.h>
33+
#include <string.h>
34+
#include <strings.h> /* bcopy, bzero */
35+
#include <stdint.h>
36+
#include <signal.h>
37+
#include <setjmp.h>
38+
#include <unistd.h>
39+
#include <sys/mman.h>
40+
41+
#ifndef MAP_ANONYMOUS
42+
#define MAP_ANONYMOUS MAP_ANON
43+
#endif
44+
45+
static sigjmp_buf g_jmp;
46+
static volatile sig_atomic_t g_faulted;
47+
48+
static void on_segv(int sig) {
49+
(void) sig;
50+
g_faulted = 1;
51+
siglongjmp(g_jmp, 1);
52+
}
53+
54+
static unsigned char *map_guarded(long pg, unsigned char **guard) {
55+
unsigned char *base = mmap(NULL, 2 * pg, PROT_READ | PROT_WRITE,
56+
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
57+
if (base == MAP_FAILED) return NULL;
58+
if (mprotect(base + pg, pg, PROT_NONE) != 0) { munmap(base, 2 * pg); return NULL; }
59+
*guard = base + pg;
60+
return base;
61+
}
62+
63+
/* Print+flush a breadcrumb BEFORE each call so that, if clib4 does not deliver
64+
SIGSEGV and the process hard-crashes, the LAST line in the output names the
65+
exact routine + length that over-ran the guard. */
66+
#define TRY(label, expr) \
67+
do { \
68+
printf("> %s len=%d (buf&15=%ld)\n", label, len, (long)(boundary & 15));\
69+
fflush(stdout); \
70+
g_faulted = 0; \
71+
if (sigsetjmp(g_jmp, 1) == 0) { expr; } \
72+
else { fails++; printf("[FAIL] %s OVER-RAN guard, len=%d (buf&15=%ld)\n",\
73+
label, len, (long)(boundary & 15)); } \
74+
} while (0)
75+
76+
int main(void) {
77+
long pg = sysconf(_SC_PAGESIZE);
78+
if (pg <= 0) pg = 4096;
79+
80+
unsigned char *rg, *rpage = map_guarded(pg, &rg); /* read buffer + guard */
81+
unsigned char *wg, *wpage = map_guarded(pg, &wg); /* write buffer + guard */
82+
if (!rpage || !wpage) { perror("mmap/mprotect"); return 2; }
83+
84+
/* read page: non-zero everywhere (so strcpy only stops at the NUL we plant) */
85+
for (long i = 0; i < pg; i++) rpage[i] = (unsigned char)((i % 255) + 1);
86+
87+
unsigned char *safe = malloc(pg + 64); /* slack buffer for dst / cmp */
88+
if (!safe) { perror("malloc"); return 2; }
89+
memset(safe, 0x5A, pg + 64);
90+
91+
struct sigaction sa, old;
92+
memset(&sa, 0, sizeof sa);
93+
sa.sa_handler = on_segv;
94+
sigemptyset(&sa.sa_mask);
95+
if (sigaction(SIGSEGV, &sa, &old) != 0)
96+
fprintf(stderr, "warning: sigaction(SIGSEGV) failed; a fault hard-crashes\n");
97+
98+
const int MAXLEN = (pg < 1024) ? (int) pg - 1 : 1024;
99+
int fails = 0;
100+
uintptr_t boundary;
101+
102+
printf("altivec_guard: page=%ld; sweeping len 1..%d at a PROT_NONE boundary\n",
103+
pg, MAXLEN);
104+
printf(" routines under test (AltiVec on G4): bcopy memcmp strcpy bzero\n");
105+
printf(" (a hard crash to the Grim Reaper instead of a report = clib4 did\n"
106+
" not deliver SIGSEGV; the crash itself is the over-run.)\n");
107+
108+
for (int len = 1; len <= MAXLEN; len++) {
109+
if ((len & 63) == 0) fprintf(stderr, "len=%d\n", len);
110+
111+
/* ---- bcopy: source ends at guard (over-READ) ---- */
112+
unsigned char *rsrc = rg - len;
113+
boundary = (uintptr_t) rsrc;
114+
TRY("bcopy", bcopy(rsrc, safe, len));
115+
116+
/* ---- memcmp: 2nd buffer ends at guard; make them equal so all len
117+
bytes are read (memcmp stops at first diff) (over-READ) ---- */
118+
memcpy(safe, rsrc, len); /* scalar memcpy -> safe */
119+
boundary = (uintptr_t) rsrc;
120+
TRY("memcmp", (void) memcmp(safe, rsrc, len));
121+
122+
/* ---- strcpy: NUL-terminated string ends at guard (over-READ) ---- */
123+
rg[-1] = 0; /* plant NUL at last readable byte */
124+
{
125+
char *ssrc = (char *) (rg - 1 - len); /* len non-zero chars + NUL@rg-1 */
126+
boundary = (uintptr_t) ssrc;
127+
TRY("strcpy", strcpy((char *) safe, ssrc));
128+
}
129+
rg[-1] = (unsigned char)((((pg - 1) % 255) + 1)); /* restore non-zero */
130+
131+
/* ---- bzero: dest ends at guard (over-WRITE) ---- */
132+
unsigned char *wdst = wg - len;
133+
boundary = (uintptr_t) wdst;
134+
TRY("bzero", bzero(wdst, len));
135+
}
136+
137+
sigaction(SIGSEGV, &old, NULL);
138+
printf("altivec_guard: %d failures across bcopy/memcmp/strcpy/bzero\n", fails);
139+
printf("altivec_guard RESULT: %s\n", fails == 0 ? "PASS" : "FAIL");
140+
return fails == 0 ? 0 : 1;
141+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* memcpy_guard.c -- expose tail over-read / over-write in clib4 memcpy/memmove.
3+
*
4+
* Why the existing test does not catch it:
5+
* test_programs/memory/memcpy is a *benchmark*. Its buffers carry 256 bytes
6+
* of slack after the source (std::vector<char> src(size + 256)) and it never
7+
* verifies the copied result -- so an implementation that touches a few bytes
8+
* past the end of the source (or destination) is completely invisible to it.
9+
*
10+
* The bug this reproduces:
11+
* clib4's AltiVec memcpy (library/cpu/altivec/vec_memcpy.sx, selected at run
12+
* time on AltiVec CPUs by clib4.c:487 `IClib4->bcopy = vec_bcopy`) uses the
13+
* classic "load two vectors + vperm" unaligned idiom, which can READ up to 15
14+
* bytes past the end of the source. Harmless in the middle of a heap, but it
15+
* FAULTS (DSI) when the source ends just before an unmapped page -- which is
16+
* what happens copying class/string data inside a VM. It crashed JamVM on
17+
* the QEMU 'amigaone' (G4) target: DSI at an lvx in clib4.library, DAR on a
18+
* page boundary, while loading java2d class bytes.
19+
*
20+
* How this test forces the failure:
21+
* It mmaps [data page][guard page] with the guard at PROT_NONE, then places
22+
* the SOURCE so it ends EXACTLY at the guard boundary (phase A) and the
23+
* DESTINATION so it ends exactly at the guard boundary (phase B). Any read or
24+
* write one byte past the requested length hits the guard and faults. A
25+
* SIGSEGV handler + siglongjmp lets the sweep continue and report every
26+
* offending length; it also (for the first time) checks that the copy is
27+
* actually correct.
28+
*
29+
* If clib4 does NOT deliver SIGSEGV for the fault, the process hard-crashes
30+
* (Grim Reaper) at the first offending case instead of reporting -- which is
31+
* itself the proof. The periodic "phase/len" breadcrumb on stderr narrows
32+
* down which case did it.
33+
*
34+
* Scope: only exercises the AltiVec path on AltiVec CPUs (G4 and the QEMU
35+
* 'amigaone' machine). On non-AltiVec CPUs (G3, 440/460, X5000/P5020,
36+
* A1222/P1022) clib4 uses scalar mem ops and this test should PASS.
37+
*
38+
* Build: drop in test_programs/memory/; `make compile-tests` builds it
39+
* (ppc-amigaos-gcc -mcrt=clib4 -fno-builtin ...). PASS == rc 0.
40+
*/
41+
#include <stdio.h>
42+
#include <stdlib.h>
43+
#include <string.h>
44+
#include <stdint.h>
45+
#include <signal.h>
46+
#include <setjmp.h>
47+
#include <unistd.h>
48+
#include <sys/mman.h>
49+
50+
#ifndef MAP_ANONYMOUS
51+
#define MAP_ANONYMOUS MAP_ANON
52+
#endif
53+
54+
static sigjmp_buf g_jmp;
55+
static volatile sig_atomic_t g_faulted;
56+
57+
static void on_segv(int sig) {
58+
(void) sig;
59+
g_faulted = 1;
60+
siglongjmp(g_jmp, 1); /* unwind back to the sigsetjmp in the sweep */
61+
}
62+
63+
/* Map [data page][guard page]; guard is PROT_NONE so any access faults.
64+
Returns the data page base (NULL on failure); *guard = the first byte that
65+
must never be touched (== one past the end of the data page). */
66+
static unsigned char *map_guarded(long pg, unsigned char **guard) {
67+
unsigned char *base = mmap(NULL, 2 * pg, PROT_READ | PROT_WRITE,
68+
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
69+
if (base == MAP_FAILED)
70+
return NULL;
71+
if (mprotect(base + pg, pg, PROT_NONE) != 0) {
72+
munmap(base, 2 * pg);
73+
return NULL;
74+
}
75+
*guard = base + pg;
76+
return base;
77+
}
78+
79+
int main(void) {
80+
long pg = sysconf(_SC_PAGESIZE);
81+
if (pg <= 0) pg = 4096;
82+
83+
unsigned char *sg, *src_page = map_guarded(pg, &sg); /* source + guard */
84+
unsigned char *dg, *dst_page = map_guarded(pg, &dg); /* dest + guard */
85+
if (!src_page || !dst_page) { perror("mmap/mprotect"); return 2; }
86+
87+
for (long i = 0; i < pg; i++) {
88+
src_page[i] = (unsigned char)(i * 7 + 1);
89+
dst_page[i] = 0;
90+
}
91+
unsigned char *scratch = malloc(pg + 64);
92+
if (!scratch) { perror("malloc"); return 2; }
93+
94+
struct sigaction sa, old;
95+
memset(&sa, 0, sizeof sa);
96+
sa.sa_handler = on_segv;
97+
sigemptyset(&sa.sa_mask);
98+
if (sigaction(SIGSEGV, &sa, &old) != 0)
99+
fprintf(stderr, "warning: sigaction(SIGSEGV) failed; a fault will hard-crash\n");
100+
101+
const int MAXLEN = (pg < 1024) ? (int) pg : 1024;
102+
int overread = 0, overwrite = 0, miscopy = 0, cases = 0;
103+
104+
printf("memcpy_guard: page=%ld; sweeping len 1..%d at a PROT_NONE boundary\n",
105+
pg, MAXLEN);
106+
printf(" (if this hard-crashes to the Grim Reaper, clib4 did not deliver\n"
107+
" SIGSEGV and the crash itself is the over-read.)\n");
108+
109+
/* ---- Phase A: source ends AT the guard -> catch tail OVER-READ ---- */
110+
for (int len = 1; len <= MAXLEN; len++) {
111+
unsigned char *src = sg - len; /* src + len == guard */
112+
cases++;
113+
if ((len & 63) == 0) { fprintf(stderr, "A len=%d\n", len); }
114+
g_faulted = 0;
115+
if (sigsetjmp(g_jmp, 1) == 0) {
116+
memcpy(scratch, src, len);
117+
if (memcmp(scratch, src, len) != 0) {
118+
miscopy++;
119+
printf("[FAIL] memcpy wrong result, len=%d\n", len);
120+
}
121+
} else {
122+
overread++;
123+
printf("[FAIL] memcpy OVER-READ past source end, len=%d (src&15=%ld)\n",
124+
len, (long) ((uintptr_t) src & 15));
125+
}
126+
g_faulted = 0;
127+
if (sigsetjmp(g_jmp, 1) == 0) {
128+
memmove(scratch, src, len); /* bcopy/memmove -> same asm */
129+
} else {
130+
overread++;
131+
printf("[FAIL] memmove OVER-READ past source end, len=%d\n", len);
132+
}
133+
}
134+
135+
/* ---- Phase B: dest ends AT the guard -> catch tail OVER-WRITE ---- */
136+
for (int len = 1; len <= MAXLEN; len++) {
137+
unsigned char *dst = dg - len; /* dst + len == guard */
138+
cases++;
139+
if ((len & 63) == 0) { fprintf(stderr, "B len=%d\n", len); }
140+
g_faulted = 0;
141+
if (sigsetjmp(g_jmp, 1) == 0) {
142+
memcpy(dst, src_page, len);
143+
if (memcmp(dst, src_page, len) != 0) {
144+
miscopy++;
145+
printf("[FAIL] memcpy wrong result (B), len=%d\n", len);
146+
}
147+
} else {
148+
overwrite++;
149+
printf("[FAIL] memcpy OVER-WRITE past dest end, len=%d (dst&15=%ld)\n",
150+
len, (long) ((uintptr_t) dst & 15));
151+
}
152+
}
153+
154+
sigaction(SIGSEGV, &old, NULL);
155+
printf("memcpy_guard: %d cases, over-read=%d over-write=%d miscopy=%d\n",
156+
cases, overread, overwrite, miscopy);
157+
int ok = (overread == 0 && overwrite == 0 && miscopy == 0);
158+
printf("memcpy_guard RESULT: %s\n", ok ? "PASS" : "FAIL");
159+
return ok ? 0 : 1;
160+
}

0 commit comments

Comments
 (0)