Skip to content

[Bug]: linux-arm64 CLI from nightly 20261022.3512 cannot exec on 16 KiB-page kernels (Raspberry Pi 5) #23433

Open

Description

@christhomas

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

Build, CI, or release tooling

Steps to reproduce

On a linux-arm64 host whose kernel uses 16 KiB pages (getconf PAGESIZE prints 16384). This includes a Raspberry Pi 5 on its default rpi-2712 kernel.

  1. npx t3@0.1.2-nightly.20261022.3512 --version
  2. Or run the extracted binary directly: ~/.t3/runtime/versions/0.1.2-nightly.20261022.3512/t3 --version

Expected behavior

It prints t3 v0.1.2-nightly.20261022.3512, as 0.1.2-nightly.20261021.3499 does on the same machine.

Actual behavior

The kernel refuses to load the binary. The shell shows Segmentation fault and exit code 139:

$ strace -f ~/.t3/runtime/versions/0.1.2-nightly.20261022.3512/t3 --version
execve(".../0.1.2-nightly.20261022.3512/t3", [..., "--version"], ...) = -1 EINVAL (Invalid argument)
+++ killed by SIGSEGV +++

Through npx it prints nothing at all. The npm launcher (bin/t3.mjs) turns the child's SIGSEGV into process.exit(139) without a message, so the command just seems to do nothing.

Cause

The two newest LOAD segments in the 3512 binary are aligned to 4 KiB (0x1000). A kernel with 16 KiB pages cannot map them. In 3499, every LOAD segment is aligned to 64 KiB.

readelf -lW t3, 3499 (works):

  LOAD  0x000000  0x0000000000400000 ... 0x409510  0x409510  R    0x10000
  LOAD  0x410000  0x0000000000810000 ... 0x2978c90 0x2978c90 R E  0x10000
  LOAD  0x2d90000 0x0000000003190000 ... 0x3dfd208 0x3dfd208 R    0x10000
  LOAD  0x6b8d208 0x0000000006f8d208 ... 0x06dca8  0x0bedf8  RW   0x10000
  LOAD  0x8dde000 0x0000000010dde000 ... 0xffe000  0xffe000  R    0x10000   <- .note.node.sea

3512 (fails):

  PHDR  0x6dad000 0x00000000071ad000 ... 0x0002a0  0x0002a0  R    0x8
  LOAD  0x000000  0x0000000000400000 ... 0x41d390  0x41d390  R    0x10000
  LOAD  0x420000  0x0000000000820000 ... 0x29d82f0 0x29d82f0 R E  0x10000
  LOAD  0x2e00000 0x0000000003200000 ... 0x3eda298 0x3eda298 R    0x10000
  LOAD  0x6cdee00 0x00000000070eee00 ... 0x0bdb00  0x0bdb00  RW   0x10000
  LOAD  0x6dad000 0x00000000071ad000 ... 0x000508  0x000508  R    0x1000    <- new segment holding the moved program headers
  LOAD  0x8faf000 0x000000001015d000 ... 0x1003000 0x1003000 R    0x1000    <- .note.node.sea, was 0x10000

The only build change between the two nightlies is #23165 (ed9b7ca), which moved the single-executable from Node 26.8.2 to 26.12.1. The binaries confirm this: 3499 embeds v26.8.2, 3512 embeds v26.12.1. So the --build-sea injector in Node 26.12.1 now writes 4 KiB-aligned segments on linux-arm64. 26.8.2 did not.

The arm64 smoke test can't catch this, because GitHub's arm64 runners use 4 KiB pages. A build-time check would catch it: fail the linux-arm64 archive if any LOAD in readelf -lW has an alignment below 0x10000.

Impact

Blocks work completely

Version or commit

0.1.2-nightly.20261022.3512 (3b95e78). Last working: 0.1.2-nightly.20261021.3499 (b5a226e).

Environment

Raspberry Pi 5 Model B Rev 1.1, Debian 13 (trixie), kernel 6.18.50+rpt-rpi-2712 (16 KiB pages), node v22.23.2 for npx

Logs or stack traces

See above. dmesg shows nothing; the failure is at execve.

Activity

  1. Skeptomenos commented on Oct 22, 2026

    @Skeptomenos

    Same crash on Apple Silicon under Asahi Linux, which also runs a 16 KiB-page kernel. It now affects the preview channel too, not only nightly, and it is still present in the newest nightly.

    Environment

    • MacBook Pro (Apple Silicon), Fedora Linux Asahi Remix 44, kernel 7.1.13-402.asahi.fc44.aarch64+16k
    • getconf PAGESIZE → 16384; glibc 2.43
    • Install: npm global (npm install -g t3@<version>), run as a t3 serve user service

    Builds tested (t3 --version)

    Build Result
    0.1.1 (latest) OK
    0.1.2-nightly.20261021.3499 OK
    0.1.2-preview.20261022.3516 (preview) SIGSEGV, exit 139
    0.1.2-nightly.20261022.3541 (release archive, checksum verified against SHA256SUMS) SIGSEGV, exit 139

    readelf -lW t3, preview 3516 (3541 is identical apart from the size of the last segment):

    LOAD 0x000000  0x0000000000400000 ... R   0x10000
    LOAD 0x420000  0x0000000000820000 ... R E 0x10000
    LOAD 0x2e00000 0x0000000003200000 ... R   0x10000
    LOAD 0x6cdee00 0x00000000070eee00 ... RW  0x10000
    LOAD 0x6dad000 0x00000000071ad000 ... R   0x1000
    LOAD 0x8faf000 0x000000001015d000 ... R   0x1000
    

    Working 3499, all segments 64 KiB-aligned:

    LOAD 0x000000  0x0000000000400000 ... R   0x10000
    LOAD 0x410000  0x0000000000810000 ... R E 0x10000
    LOAD 0x2d90000 0x0000000003190000 ... R   0x10000
    LOAD 0x6b8d208 0x0000000006f8d208 ... RW  0x10000
    LOAD 0x8dde000 0x0000000010dde000 ... R   0x10000
    

    This matches the layout in this issue and in #23477 / #23773. Because the defect has reached preview, a promotion to latest would break every Linux arm64 host with 16 KiB pages (Asahi on Apple Silicon, Raspberry Pi 5 default kernel). We have pinned latest 0.1.1 until a fixed build ships, and can test a candidate build on Asahi.

    — drafted by David Helmus's agent

  2. Project516 commented on Oct 22, 2026

    @Project516
    Contributor

    This now affects the stable channel too. The v0.1.2 release (Latest) has the same problem on a Raspberry Pi 5.

    • Kernel 6.18.50+rpt-rpi-2712, getconf PAGESIZE prints 16384
    • t3-0.1.2-linux-arm64.tar.gz (sha256 matches SHA256SUMS): ./t3 --version exits 139 with Segmentation fault, every time
    • 0.1.1 works on the same machine

    t3 update --yes from 0.1.1 fails in the staged binary check (PinnedRuntimeInstallError ... Process interrupted due to receipt of signal: 'SIGSEGV'), so the background service stays on 0.1.1. The update aborts cleanly, but stable Pi 5 users are stuck on 0.1.1 until this is fixed.

  3. stefansedich commented on Oct 22, 2026

    @stefansedich

    This is also an issue trying to use the t3 kit with a Docker Sandbox on an M5 Mac https://hub.docker.com/r/sbx/t3code-kit

  4. dbjohnson commented on Oct 23, 2026

    @dbjohnson

    Local workaround for 16 KiB-page hosts until #23443 ships. Tested on Apple Silicon running Arch Linux ARM with the Asahi kernel (7.2.12-1-1-ARCH, getconf PAGESIZE = 16384, glibc 2.43), with 0.1.2-nightly.20261022.3541.

    The crash comes from the two LOAD segments that LIEF adds during SEA injection:

    • The relocated PHDR segment at vaddr 0x71ad000 shares a 16 KiB page with the end of .bss, which ends at 0x71ac900. The kernel maps it read-only over that page, and the first write to the tail of .bss faults.
    • The .note.node.sea segment has file offset 0x8faf000 and vaddr 0x1015d000, and the two aren't congruent mod 16 KiB.

    The script below rewrites only virtual addresses in the headers; file contents stay where they are. It gives each 4 KiB-aligned LOAD segment a vaddr congruent to its file offset mod 64 KiB, placed past the previous segment's last 64 KiB page, and sets its alignment to 0x10000. PT_PHDR, PT_NOTE and section addresses inside a moved segment move with it. Node finds the SEA blob by walking PT_NOTE headers, so it still finds it. The moved PHDR segment no longer sits at first LOAD vaddr + e_phoff, so kernels older than 5.18 (which compute AT_PHDR that way) won't load the result. I've only tested on 7.2.

    After patching, 3541 gives:

    $ ./t3 --version
    t3 v0.1.2-nightly.20261022.3541
    $ ./t3 __service-preflight --database-path ~/.t3/userdata/statev2.sqlite --launcher-protocol 3
    {"status":"ready","version":"0.1.2-nightly.20261022.3541","launcherProtocol":3}
    

    The other native files in the archive (resource-monitor, node-pty, keyring, ffi-rs, fff, the Cursor SDK binaries) are already 64 KiB-aligned and needed no changes.

    To get a t3 service install onto the new version, pre-stage the patched build. The updater skips the download when ~/.t3/runtime/versions/<version>/t3 exists with a matching .install-complete, and then runs the preflight against the staged binary:

    V=0.1.2-nightly.20261022.3541
    mkdir t3-$V && cd t3-$V
    curl -fsSLO https://github.com/pingdotgg/t3code/releases/download/v$V/SHA256SUMS
    curl -fsSLO https://github.com/pingdotgg/t3code/releases/download/v$V/t3-$V-linux-arm64.tar.gz
    grep linux-arm64 SHA256SUMS | sha256sum -c
    mkdir x && tar -xf t3-$V-linux-arm64.tar.gz -C x --strip-components=1
    python3 -I /path/to/realign-elf-16k.py x/t3
    printf '%s\n' $V > x/.install-complete
    chmod 700 x && mv -T x ~/.t3/runtime/versions/$V

    Then run the update from the app. My service went from 0.1.1-nightly.20261020.3411 to 3541, and the launcher recorded it as committed.

    realign-elf-16k.py
    """Move 4K-aligned LOAD segments of an aarch64 ELF so they load on 16K/64K-page kernels.
    
    Only virtual addresses change; file contents stay put. Each 4K-aligned LOAD gets a new
    vaddr congruent to its file offset mod 64K, placed past the end of the preceding segment's
    64K page. PT_PHDR, PT_NOTE and section addresses inside a moved segment move with it.
    """
    import struct, sys
    
    ALIGN = 0x10000
    path = sys.argv[1]
    data = bytearray(open(path, "rb").read())
    assert data[:4] == b"\x7fELF" and data[4] == 2 and data[5] == 1
    e_phoff, e_shoff = struct.unpack_from("<QQ", data, 0x20)
    e_phentsize, e_phnum, e_shentsize, e_shnum = struct.unpack_from("<HHHH", data, 0x36)
    
    PH = "<IIQQQQQQ"  # type flags offset vaddr paddr filesz memsz align
    phdrs = [list(struct.unpack_from(PH, data, e_phoff + i * e_phentsize)) for i in range(e_phnum)]
    loads = sorted((p for p in phdrs if p[0] == 1), key=lambda p: p[3])
    
    moves = []  # (old_start, old_end, delta)
    prev_end = 0
    for p in loads:
        off, vaddr, memsz, align = p[2], p[3], p[6], p[7]
        if align >= ALIGN and (vaddr - off) % ALIGN == 0 and vaddr >= prev_end:
            prev_end = -(-(vaddr + memsz) // ALIGN) * ALIGN
            continue
        base = -(-prev_end // ALIGN) * ALIGN
        new = base + (off % ALIGN)
        while new < prev_end:
            new += ALIGN
        moves.append((vaddr, vaddr + memsz, new - vaddr))
        prev_end = -(-(new + memsz) // ALIGN) * ALIGN
    
    def shift(addr):
        for lo, hi, d in moves:
            if lo <= addr < hi:
                return addr + d, d
        return addr, 0
    
    for i, p in enumerate(phdrs):
        if p[0] in (1, 4, 6):  # LOAD, NOTE, PHDR
            new, d = shift(p[3])
            if d:
                print(f"phdr[{i}] type={p[0]} vaddr {p[3]:#x} -> {new:#x}")
                p[3] = p[4] = new
                if p[0] == 1:
                    p[7] = ALIGN
        struct.pack_into(PH, data, e_phoff + i * e_phentsize, *p)
    
    for i in range(e_shnum):
        o = e_shoff + i * e_shentsize
        addr = struct.unpack_from("<Q", data, o + 0x10)[0]
        if addr:
            new, d = shift(addr)
            if d:
                struct.pack_into("<Q", data, o + 0x10, new)
                print(f"shdr[{i}] addr {addr:#x} -> {new:#x}")
    
    open(path, "wb").write(data)

    This is a stopgap; the real fix is aligning these segments at build time, as #23443 does.

  5. christhomas commented on Oct 23, 2026

    @christhomas
    Author

    Shouldn't a pipeline test have picked this up? I think this also needs to be added here cause it sounds like a very mechanical fault. Something quite easy to check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions