Skip to content

fix(altivec): vec_strcpy over-reads past the source page boundary (DSI crash on G4) - #438

Merged
afxgroup merged 1 commit into
AmigaLabs:developmentfrom
derfsss:fix/altivec-strcpy-page-overread
Jun 20, 2026
Merged

fix(altivec): vec_strcpy over-reads past the source page boundary (DSI crash on G4)#438
afxgroup merged 1 commit into
AmigaLabs:developmentfrom
derfsss:fix/altivec-strcpy-page-overread

Conversation

@derfsss

@derfsss derfsss commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Issue Description

On AltiVec-equipped PowerPC (G4 / MPC744x/745x), strcpy() — which clib4 routes
to the AltiVec vec_strcpy at runtime — can read past the end of the source
page
and take a DSI (Data Storage Interrupt) when the source string ends close
to a 4K page boundary and the next page is unmapped. The result is a hard crash
(Grim Reaper) inside clib4.library.

It is intermittent in normal code but reproduces reliably whenever short strings
are copied out of buffers that end near a page boundary — e.g. a VM interning
class-name strings. It was first hit by a JamVM-based Java runtime copying
class-name UTF-8 strings during class loading on a 7447/7457 G4:

Crash occured in module clib4.library at address 0x7F9CB40C
Type of crash: DSI (Data Storage Interrupt) exception
  ...
*7f9cb40c: 7c4050ce   lvx   v2,r0,r10      ; r10 -> the guard (unmapped) page
CPU Model: Motorola MPC 7447/7457 Apollo (Extensions: altivec)

Cause

vec_strcpy (library/cpu/altivec/vec_strcpy.sx, the Motorola/Chuck Corley
AltiVec reference routine) speculatively loads source vectors ahead of the NUL
terminator and is supposed to guard against crossing a 4K page so it never
faults on an unmapped page. The guard was wrong in two ways:

  1. It was computed from the destination, not the source. The faulting lvx
    loads read from the SOURCE (DS), but the "QWs to next page" counter (PBC)
    was derived from the DESTINATION pointer (DD):

    addi   ADD,DD,PAGE_SIZE   ; dest's next 4K page
    ...
    subf.  PBC,DD,ADD         ; QWs from dest to the DEST's page boundary
    

    The destination is QW-aligned, so the count is exact for the dest — but it
    has no relation to where the source sits in its own page. For the common case
    strcpy(fresh_buffer, string_ending_near_a_page_end) the destination is
    mid-page, so the counter is far too large and the loop keeps loading source
    vectors straight into the next (possibly unmapped) page.

  2. The source pointer is not QW-aligned. lvx ignores the low 4 address
    bits (it loads DS & ~15). A naive source-based (page - DS) / 16 rounds
    down: when the source is fewer than 16 bytes from its page boundary the
    count becomes 0, which falls through to the New_page_0 path that reloads a
    whole page and over-reads anyway.

Solution

Compute the page-crossing guard from the source, using the QW-aligned
source address (matching what lvx actually loads):

-	addi	ADD,DD,PAGE_SIZE	// dst addr in next 4K page
+	addi	ADD,SRC,PAGE_SIZE	// SRC addr in next 4K page (loads fault on src)
 ...
-	subf.	PBC,DD,ADD	// Now bytes to next 4K page (from dest)
+	rlwinm	Rt,DS,0,0,27	// align src down to QW (lvx loads use DS&~15)
+	subf.	PBC,Rt,ADD	// QW-exact bytes from aligned src to its page boundary

The loop now pauses exactly at the source's page boundary, checks the last
in-page vector for the NUL (which is present, because the string ends in that
page), and finishes the tail byte-by-byte — never reading into the next page.
AltiVec acceleration is preserved (no fallback to scalar).

The length-bounded AltiVec routines (vec_memcpy/vec_memcmp/vec_bcopy/
vec_bzero) cannot over-read this way and are verified clean (see Testing).
The length-unbounded scanners vec_strchr/vec_memchr (currently behind
SLOWER_ALTIVEC_FUNCTIONS) deserve the same review if they are ever enabled.

Testing

Verified under emulation — no physical AltiVec hardware was available. The
tests were built and run under QEMU (qemu-system-ppc -M amigaone), whose
CPU model emulates a Motorola MPC 7447/7457 (G4) with AltiVec. QEMU
executes the real AltiVec instructions (lvx/vperm) and emulates the MMU
page fault, so the AltiVec vec_strcpy path and the page-boundary DSI are
genuinely exercised (not stubbed) — but a confirmation on real G4 silicon would
be welcome.

Adds two guard-page tests under test_programs/memory/ (auto-built by
make compile-tests, which compiles with -fno-builtin so the calls reach
clib4's routines). Each mmaps a [data page][PROT_NONE guard page] pair and
places the buffer so it ends exactly at the guard, so any read or write one byte
past the requested length faults immediately. A SIGSEGV handler reports the
offending routine + length; if the fault is not delivered as a signal, a flushed
breadcrumb names the last call attempted.

  • altivec_guard.c — sweeps the AltiVec-routed routines (bcopy, memcmp,
    strcpy, bzero) over lengths 1..1024 with the buffer at the guard boundary.
  • memcpy_guard.c — same technique for memcpy/memmove, plus a
    correctness check for them.

How it was run: built with make compile-tests, deployed the resulting ELF
to the AmigaOS 4.1 guest, and launched from the Amiga Shell via Run (so it gets
a CLI process), with output captured to a file. The rebuilt clib4.library
(carrying this fix) was installed to LIBS: and loaded with a reboot.

Result on the emulated G4:

altivec_guard Result
Before DSI crash at strcpy len=16 (lvx into the guard page; confirmed in the Grim Reaper crash log — lvx v2,r0,r10 with r10 pointing at the guard)
After RESULT: PASS — 0 failures across bcopy/memcmp/strcpy/bzero × len 1..1024

memcpy_guard passes before and after (clib4 routes memcpy/memmove to the
scalar path). The Java runtime that originally surfaced the crash no longer
faults during class loading.

🤖 Generated with Claude Code

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>
@afxgroup
afxgroup merged commit 4c1322c into AmigaLabs:development Jun 20, 2026
2 checks passed
@derfsss

derfsss commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Regression-checked this change against the clib4 test_programs suite, built and run on a QEMU MPC 7447/7457 (G4, AltiVec) guest (no physical AltiVec hardware here).

Build: all 400 buildable test programs compile cleanly against the rebuilt library — no build regression.

Fix verification (all PASS on the emulated G4):

  • altivec_guard (this PR): full bcopy / memcmp / strcpy / bzero × len 1..1024 guard-page sweep — PASS (this was a DSI crash at strcpy len=16 before the fix).
  • memcpy_guard (this PR): PASS.
  • A strcpy/strncpy correctness test — 8831 cases covering every src/dst alignment × length, strncpy, and 300 strings whose NUL sits exactly on a PROT_NONE page boundary — 0 failures. Confirms the change copies byte-exact, not merely crash-free.

The rebuilt clib4.library differs from the pre-fix build in only vec_strcpy.o, so the entire regression surface is strcpy/strncpy — verified correct and crash-free above. No regression.

For transparency: string/strcmp faults during the sweep, but it is pre-existing and unrelated to this change — the test deliberately calls strcmp(foo, NULL) (undefined behaviour) and the fault is in clib4's scalar strcmp, a different routine that is byte-identical to the pre-fix library (so it reproduces on unmodified clib4). Some other suite tests are interactive (read stdin) or need network/files and were not run headless.

🤖 Generated with Claude Code

derfsss added a commit to derfsss/java-os4 that referenced this pull request Jun 20, 2026
Addresses the amigans.net 0.5.0 reports:

- close-freeze (#1): native at-exit window teardown in libamigaawt, so a
  Swing JFrame with the default EXIT_ON_CLOSE (System.exit without dispose)
  no longer leaves an open Intuition window that hangs the machine.
- keyboard in games (#4): AmigaEventPump dispatches a synthesized KeyEvent to
  a JComponent (the focused window's JRootPane) when the focus owner is null,
  so WHEN_IN_FOCUSED_WINDOW bindings fire (were lost to the bare java.awt.Frame).
- class-version gate (#5): JamVM rejects class-file major > 52 with
  UnsupportedClassVersionError instead of a far-away BootstrapMethodError
  (carried in docs/jamvm-amiga-openjdk.patch).
- clib4.library is now bundled and installed to LIBS: when absent (#2), and
  the `java` launcher gets the AmigaDOS script bit + a C: copy so it runs from
  any Shell; example jars and the VM test suite ship in the release (#3).
  (install.py / JavaOS4InstallerLocale.py / package.sh.)

Tests + examples: VmSuite (broad VM coverage), KeyBindTest, CloseTest,
VersionGateTest, HelloJava, SwingDemo, built by tools/build-tests.sh and
bundled under examples/.

The release now ships the dev clib4.library (incl. the AltiVec vec_strcpy
page-overread fix, AmigaLabs/clib4#438) in lockstep with build/sobjs.

Validated on QEMU (MPC 7447/7457 G4): #1/#4/#5 pass; VmSuite 26/29 (3
pre-existing OpenJDK-8-on-Amiga gaps: createTempFile->SecureRandom, GC
weak-ref timing). VERSION -> 0.5.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants