Skip to content

fix(fakecgo): use per-OS values for pthread_sigmask how (macOS/FreeBSD signal mask) - #90

Merged
kolkov merged 2 commits into
mainfrom
fix/fakecgo-sigmask-constants
Oct 15, 2026
Merged

kolkov merged 2 commits into
mainfrom
fix/fakecgo-sigmask-constants

Conversation

@kolkov

@kolkov kolkov commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On macOS and FreeBSD with CGO_ENABLED=0, every new Go runtime thread started with all signals unblocked instead of blocked. internal/fakecgo defined the pthread_sigmask how constants once, with the Linux values:

SIG_BLOCK   sighow = 0
SIG_UNBLOCK sighow = 1
SIG_SETMASK sighow = 2

Darwin, FreeBSD and NetBSD use 1/2/3 (see the header links in the diff). So in _cgo_sys_thread_start (go_darwin_*.go, go_freebsd_*.go):

  • pthread_sigmask(SIG_SETMASK, &all, &old) ran as SIG_UNBLOCK, which unblocked every signal across pthread_create instead of blocking them, and the new thread inherited that mask;
  • pthread_sigmask(SIG_SETMASK, &old, nil) ran as SIG_UNBLOCK too, which unblocked the signals in the saved mask instead of reinstating it.

A signal that lands on the new thread before the runtime has set up its g0 stack can crash the process. purego hit exactly this on macOS under the profiler: signal 27 received on thread with no signal stack (purego#552, golang/go#77588). goffi's internal/fakecgo derives from purego's and carries the same code. Linux is unaffected (the values match), and so is Android (its own libcgo_android.go already has the correct values).

Fix

Ported from ebitengine/purego#553: the constants move out of the shared libcgo.go into libcgo_{darwin,freebsd,linux,netbsd}.go, each with that system's values and a permalink to the header that defines them. The build constraints of those four files together are exactly the constraint of libcgo.go, so the constants are defined on every target that defines sighow, once.

libcgo_netbsd.go gets the values too. goffi does not support NetBSD yet (the module doesn't build for it on main), but #88 adds it on top of this fakecgo and would otherwise ship the same bug.

New test: TestSighowMatchesSystemHeaders compiles a tiny C probe with the system compiler and compares the three constants with the host's <signal.h>, so it doesn't just restate the numbers. It is skipped when no C compiler is available.

CI used to run tests only for ./ffi and ./types, so no test under internal/ ever ran in CI. This PR adds a separate Run internal package tests step (go test ./internal/..., no coverage, so the coverage numbers don't change). The new test now runs against the real <signal.h> in the ubuntu and macOS cgo=0 jobs. The same step also runs the existing internal/arch/amd64 and internal/arch/arm64 tests in CI for the first time (arm64 on the macOS runner).

Verification

Linux (WSL Ubuntu 22.04, Go 1.27.1):

  • TestSighowMatchesSystemHeaders passes.
  • Negative control: with the BSD values injected into libcgo_linux.go, the test fails with SIG_SETMASK = 3, but <signal.h> on this system says 2, so it catches this class of bug.
  • go test -count=1 ./... passes with CGO_ENABLED=0 and CGO_ENABLED=1, and go vet ./... is clean.
  • golangci-lint over 9 targets (linux, darwin, freebsd × amd64/arm64, android/arm64, windows/amd64, windows/arm64), with the repo config minus its build-tags: [linux] line so each GOOS lints its own files: the findings are identical to main, none of them in internal/fakecgo. The existing findings on other targets are tracked separately.
  • Cross-compile: go build ./... for linux, darwin and freebsd (amd64 and arm64), android/arm64 and windows (amd64, arm64), plus -tags goffi_static and CGO_ENABLED=1. internal/fakecgo builds, vets and compiles its test binary for darwin, freebsd and netbsd (amd64 and arm64).

Not verified yet:

  • On a real Mac. CI's macos-latest will run the new test.
  • On FreeBSD or NetBSD hosts.
  • A reproduction of the crash itself, which needs the race window to be hit.

macOS validation

@lkmavi, could you run this branch on your Mac?

CGO_ENABLED=0 go test -count=1 -v -run TestSighowMatchesSystemHeaders ./internal/fakecgo
CGO_ENABLED=0 go test -count=1 ./...

The first should print PASS (not SKIP). If you have time, any gogpu app you normally run on macOS built against this branch would be a good real-world check too.

Release

Patch release (bug fix, no API change).

@kolkov kolkov added priority: high Important for next release type: bug Something isn't working area: fakecgo Fake CGO runtime integration platform: macos macOS-specific (includes Apple Silicon) labels Oct 15, 2026
@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@kolkov
kolkov force-pushed the fix/fakecgo-sigmask-constants branch from af0f71f to 090505d Compare October 15, 2026 19:01
@kolkov kolkov added platform: freebsd FreeBSD-specific area: ci CI workflows, check scripts, branch protection labels Oct 15, 2026
SIG_BLOCK, SIG_UNBLOCK and SIG_SETMASK were defined once in libcgo.go
with the Linux values 0/1/2. Darwin, FreeBSD and NetBSD use 1/2/3, so
pthread_sigmask(SIG_SETMASK, ...) in _cgo_sys_thread_start ran as
SIG_UNBLOCK there: new runtime threads started with every signal
unblocked instead of blocked, and the restore call unblocked the saved
mask instead of reinstating it. A signal arriving before the new thread
had its g0 stack set up could crash the process.

Define the constants in libcgo_{darwin,freebsd,linux,netbsd}.go with
each system's values. Linux and Android were not affected.

Add TestSighowMatchesSystemHeaders, which compiles a probe with the
system C compiler and checks the constants against the host's
<signal.h>, so CI on macOS catches a mismatch.

Ported from ebitengine/purego#553 (ebitengine/purego#552).
The test job ran only ./ffi and ./types (with coverage), so no test
under internal/ ever ran in CI, including the new
TestSighowMatchesSystemHeaders in internal/fakecgo, which is only
meaningful against each OS's real <signal.h>.

Add a separate step that runs go test ./internal/... without coverage,
so the coverage numbers are unchanged. Packages excluded on a target
by build constraints are skipped by the pattern.
@kolkov
kolkov force-pushed the fix/fakecgo-sigmask-constants branch from 090505d to 26a9bdb Compare October 15, 2026 19:10
@kolkov
kolkov merged commit e6acf0f into main Oct 15, 2026
17 checks passed
@kolkov

kolkov commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@lkmavi this is now merged to main (4563203). Could you run it on your Mac when you get a chance?

git checkout main && git pull
CGO_ENABLED=0 go test -count=1 -v -run TestSighowMatchesSystemHeaders ./internal/fakecgo
CGO_ENABLED=0 go test -count=1 ./...

TestSighowMatchesSystemHeaders should print PASS, not SKIP (SKIP means no C compiler was found). CI already runs it on macos-latest (arm64) and it passes there. A run on a real machine would be good extra confirmation, ideally with a gogpu app you normally use on macOS built against main. If anything looks off, please open an issue and we'll pick it up.

@kolkov
kolkov deleted the fix/fakecgo-sigmask-constants branch October 15, 2026 19:17
@lkmavi

lkmavi commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

@kolkov I started it, the tests passed.
Снимок экрана — 2026-10-15 в 23 24 41

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

Labels

area: ci CI workflows, check scripts, branch protection area: fakecgo Fake CGO runtime integration platform: freebsd FreeBSD-specific platform: macos macOS-specific (includes Apple Silicon) priority: high Important for next release type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants