Skip to content

all: Add support for OpenBSD - #535

Open
tannevaled wants to merge 2 commits into
ebitengine:mainfrom
tannevaled:openbsd
Open

tannevaled wants to merge 2 commits into
ebitengine:mainfrom
tannevaled:openbsd

Conversation

@tannevaled

Copy link
Copy Markdown

Closes #454.

purego does not merely lack OpenBSD — it fails to compile there, so any program that imports it at all is blocked, even on a path it never takes. struct_<arch>.go and cdecl.go carry no OS constraint (only the filename's GOARCH suffix), while the symbols they use live in syscall.go behind (darwin||freebsd||linux||netbsd||windows). That is exactly the error in #454.

Developed and tested on a real OpenBSD 7.9/arm64 machine, not cross-compiled.

Read from the machine, not adapted from NetBSD

"The BSDs agree" is a guess, so each value was checked:

RTLD_* from /usr/include/dlfcn.h — they do agree with NetBSD's
stack_t, SS_DISABLE (0x004) from sys/signal.h
PTHREAD_{MUTEX,COND}_INITIALIZER both NULL, so uintptr(0)
-ldl there is no libdl; dl* are weak symbols in libc. #cgo !netbsd LDFLAGS: -ldl → !netbsd,!openbsd, else cgo fails with unable to find library -ldl
pthread pthread_create is not in libc.so.103.0; it is in libpthread.so.28.1
__ps_strings libc exports environ and __progname but no __ps_strings, which the NetBSD file supplies

The soname looks like a blocker and is not. OpenBSD ships no unversioned libc.so file. "libc.so" is still correct: the Go runtime writes exactly that in runtime/sys_openbsd.go, and at runtime Dlopen("libc.so") and Dlopen("libc.so.103.0") return the same handle, because ld.so resolves the soname.

BTI needed its own callback table

OpenBSD/arm64 enforces Branch Target Identification, and the shared zcallback_arm64.s cannot work under it.

callbackasmAddr computes entry i as base + i*8. The assembler emits a BTI c at the TEXT symbol, so base is a landing pad — entry 0 works by accident, while entry 1 lands on entry 0's B, which is not a landing pad, and raises SIGILL.

Measured before fixing: callback 0 at 0xc3640 returned; callback 1 at 0xc3648 died; objdump showed a JMP at that address. The same program on darwin/arm64 returns all four callbacks correctly, which is what says this is OpenBSD's enforcement rather than a general arm64 bug.

So zcallback_openbsd_arm64.s gives every entry its own landing pad — three instructions, entrySize = 12 for openbsd/arm64 only. The first entry deliberately has none written for it: the assembler's own pad at the symbol serves as it. Writing a second made entry 0 sixteen bytes while the rest were twelve, which moved the fault rather than removing it — that intermediate state was measured too.

Verification

  • openbsd/arm64, native, CGO_ENABLED=0: the whole test suite passes.
  • darwin/arm64 still passes.
  • linux 386/arm/amd64/arm64, netbsd amd64/arm64, windows amd64/arm64, darwin amd64 and openbsd/amd64 all still cross-compile.

Two honest limits

  1. openbsd/amd64 cross-compiles and is not tested. It needs no BTI handling and takes the shared amd64 path, but nobody has run it.
  2. freebsd/amd64 and freebsd/arm64 fail to build on this Go with //go:cgo_export_dynamic environ only allowed in cgo-generated code. That reproduces on main without this branch, so it is not from here — but it looks worth someone's attention.

🤖 Generated with Claude Code

tannevaled and others added 2 commits September 24, 2026 10:29
Closes ebitengine#454, which has been open since May with the
error every consumer hits: purego does not merely lack OpenBSD, it
FAILS TO COMPILE there, so any program importing it at all is blocked.
Navidrome hit it through gen2brain/webp; go-doom hits it through
Ebitengine's GL loader.

Everything below was read from a real OpenBSD 7.9/arm64 machine rather
than adapted from NetBSD, because "the BSDs agree" is a guess:

  - RTLD_* from /usr/include/dlfcn.h. They DO agree with NetBSD's;
    checking was cheaper than finding out they had stopped.
  - stack_t and SS_DISABLE (0x004) from /usr/include/sys/signal.h.
  - PTHREAD_MUTEX_INITIALIZER and PTHREAD_COND_INITIALIZER are both
    NULL (pthread.h:157-158), so uintptr(0) is right.
  - dlopen/dlsym/dlerror/dlclose are weak symbols in libc; there is no
    libdl, so `#cgo !netbsd LDFLAGS: -ldl` becomes `!netbsd,!openbsd`.
    Without that the cgo build fails with "unable to find library -ldl".
  - pthread is NOT in libc: pthread_create is absent from
    libc.so.103.0 and lives in libpthread.so.28.1, so the generator
    names libpthread.so for it.
  - libc exports environ and __progname, and does NOT export
    __ps_strings, which NetBSD's equivalent file supplies.

The soname deserves its own note, because it is the thing that looks
like a blocker and is not. OpenBSD ships no unversioned libc.so FILE --
/usr/lib holds libc.so.103.0 and nothing else. "libc.so" is still what
belongs in cgo_import_dynamic: the Go runtime itself writes exactly
that in runtime/sys_openbsd.go. Verified at runtime too, which is the
part that could have gone either way: Dlopen("libc.so") and
Dlopen("libc.so.103.0") return the SAME handle, because ld.so resolves
the soname.

What passes, natively, CGO_ENABLED=0, on openbsd/arm64: Dlopen,
Dlsym, nested Dlopen, SyscallN, SyscallN's errno handling, RegisterFunc
including the concurrent pointer-return case, floats, and qsort --
which matters because qsort takes a CALLBACK, so NewCallback works.
RegisterLibFunc binds and calls strlen and puts for real.

⚠ ONE TEST STILL FAILS and this is not ready to merge because of it.
TestRegisterLibFunc_Bool dies with SIGILL. It is the one case that
calls a callback DIRECTLY from Go rather than letting C call it. This
CPU reports BT (Branch Target Identification) and PAC in its feature
line, and SIGILL is what BTI raises on an indirect branch to an
instruction that is not a landing pad -- but that is a hypothesis with
a plausible mechanism, not a diagnosis, and it is recorded as such.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes ebitengine#454.

purego does not merely lack OpenBSD: it FAILS TO COMPILE there, so any
program that imports it at all is blocked even on a path it never
takes. struct_<arch>.go and cdecl.go carry no OS constraint -- only the
filename's GOARCH suffix -- while the symbols they use live in
syscall.go behind (darwin||freebsd||linux||netbsd||windows). That is
the error in ebitengine#454, and the one Navidrome hit through gen2brain/webp.

Everything here was read from a real OpenBSD 7.9/arm64 machine rather
than adapted from NetBSD, because "the BSDs agree" is a guess:

  - RTLD_* from /usr/include/dlfcn.h. They DO agree with NetBSD's.
  - stack_t and SS_DISABLE (0x004) from sys/signal.h.
  - PTHREAD_MUTEX_INITIALIZER and PTHREAD_COND_INITIALIZER are NULL.
  - dlopen and friends are weak symbols in libc and there is no libdl,
    so `#cgo !netbsd LDFLAGS: -ldl` becomes `!netbsd,!openbsd`.
    Without it cgo fails with "unable to find library -ldl".
  - pthread_create is NOT in libc.so.103.0; it is in libpthread.so.28.1.
  - libc exports environ and __progname, and no __ps_strings, which the
    NetBSD file supplies.

The soname looks like a blocker and is not. OpenBSD ships no
unversioned libc.so FILE. "libc.so" is still right: the Go runtime
writes exactly that in runtime/sys_openbsd.go, and at RUNTIME
Dlopen("libc.so") and Dlopen("libc.so.103.0") return the same handle,
because ld.so resolves the soname.

BTI is the part that needed its own file. OpenBSD/arm64 enforces
Branch Target Identification, and the shared zcallback_arm64.s cannot
work under it. callbackasmAddr computes entry i as base+i*8; the
assembler emits a `BTI c` at the TEXT symbol, so base IS a landing pad
and entry 0 works by accident, while entry 1 lands on entry 0's B --
not a landing pad -- and raises SIGILL. Measured before fixing:
callback 0 at 0xc3640 returned, callback 1 at 0xc3648 died, and objdump
showed a JMP at that address. The same program on darwin/arm64 returns
all four callbacks correctly, which is what says this is OpenBSD's
enforcement rather than a general arm64 bug.

So zcallback_openbsd_arm64.s gives every entry its own landing pad --
three instructions, entrySize 12 for openbsd/arm64 only. The first
entry deliberately has none written for it: the assembler's own pad at
the symbol serves as it. Writing a second made entry 0 sixteen bytes
while the rest were twelve, which moved the fault rather than removing
it; that intermediate state was measured too.

Verified natively on openbsd/arm64, CGO_ENABLED=0: the whole test
suite passes. darwin/arm64 still passes. linux 386/arm/amd64/arm64,
netbsd, windows, darwin and openbsd/amd64 all still cross-compile.

Two honest limits. openbsd/amd64 cross-compiles and is NOT tested --
it needs no BTI handling and takes the shared amd64 path, but nobody
has run it. And freebsd/amd64 and freebsd/arm64 fail to build on this
Go with "//go:cgo_export_dynamic environ only allowed in cgo-generated
code"; that reproduces on main without this branch, so it is not from
here, but it is worth someone's attention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hajimehoshi

Copy link
Copy Markdown
Member

Add tests for GitHub Actions, thanks

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.

Add support for OpenBSD

2 participants