Skip to content

internal/fakecgo: preserve frame pointer across mstart call in threadentry - #486

Merged
hajimehoshi merged 4 commits into
ebitengine:mainfrom
gdams:fix/fakecgo-threadentry-framepointer
Jul 31, 2026
Merged

internal/fakecgo: preserve frame pointer across mstart call in threadentry#486
hajimehoshi merged 4 commits into
ebitengine:mainfrom
gdams:fix/fakecgo-threadentry-framepointer

Conversation

@gdams

@gdams gdams commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #485.

Problem

internal/fakecgo's threadentry calls runtime.mstart directly. For a fakecgo-created thread the M's stack is system-allocated, so when the M exits the runtime does mexit(osStack=true), which returns from mstart back into threadentry (by design, so the pthread can unwind and exit). But mstart runs the scheduler and comes back with BP = 0.

On Go 1.28 the amd64 compiler emits a frame-pointer LEAVE epilogue (MOV BP,SP; POP BP) for threadentry. With BP = 0, LEAVE sets SP = 0 and the following RET dereferences a NULL pointer:

#0  fakecgo.threadentry (...) at internal/fakecgo/go_linux.go:56    return nil
    => leave        rbp = 0x0        // SIGSEGV, si_addr = NULL
#1  threadentry_trampoline (trampolines_amd64.s)
#2  start_thread

Real cgo doesn't hit this because its x_cgo_threadentry invokes mstart through crosscall_amd64, which saves/restores the callee-saved registers (including BP) on the stack around the call. fakecgo's threadentry skipped that step.

This is mostly latent because Ms usually park and get reused rather than exiting. It surfaces whenever an M actually exits — e.g. a runtime.LockOSThread goroutine that finishes (which is how the Go standard library's os.TestProgWideChdir tripped it under CGO_ENABLED=0).

Fix

Route the ts.fn (runtime.mstart) call through a small assembly shim, callThreadEntryFn, that saves and restores the callee-saved registers on the stack around the call (using the existing PUSH_REGS_HOST_TO_ABI0 / POP_REGS_HOST_TO_ABI0 macros), mirroring crosscall_amd64. The existing call5 helper can't be reused because it stashes SP in BP (MOV SP,BP), which mstart zeroes.

  • go_linux.go, go_darwin.go, go_freebsd.go, go_netbsd.go: call callThreadEntryFn(ts.fn).
  • trampolines_amd64.s: the shim.
  • threadentry_amd64.go: asm function declaration.
  • threadentry_noasm.go: plain-call fallback for non-amd64 (unaffected — those epilogues don't rely on BP).
  • threadentry_test.go: regression test that forces locked OS threads to exit.

Testing

  • New TestThreadEntryReturn crashes reliably before this change and passes after.
  • With the fix vendored into a CGO_ENABLED=0 + Go 1.28-devel toolchain, the standard library's os, syscall and runtime suites pass again.
  • Cross-builds verified for linux/amd64, linux/arm64, linux/386, darwin/amd64, netbsd/amd64; gofmt clean.

@gdams
gdams marked this pull request as draft July 31, 2026 09:20
@gdams
gdams force-pushed the fix/fakecgo-threadentry-framepointer branch 2 times, most recently from 64ae08d to a706472 Compare July 31, 2026 09:39
…entry

threadentry calls runtime.mstart directly. For a fakecgo thread the M's stack is system-allocated, so when the M exits mexit(osStack=true) returns from mstart back into threadentry. mstart returns with BP clobbered (0); on amd64 the frame-pointer LEAVE epilogue (MOV BP,SP; POP BP) then dereferences a NULL pointer and crashes.

Route the mstart call through an assembly shim that saves and restores the callee-saved registers (including BP) on the stack, mirroring what real cgo's crosscall_amd64 does. The existing call5 helper cannot be reused because it stashes SP in BP, which mstart zeroes.

Add a regression test that forces locked OS threads to exit.

Fixes ebitengine#485
@gdams
gdams force-pushed the fix/fakecgo-threadentry-framepointer branch from a706472 to 455f9c0 Compare July 31, 2026 09:52
@gdams
gdams marked this pull request as ready for review July 31, 2026 10:10
Comment thread internal/fakecgo/threadentry_test.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an amd64-specific crash in internal/fakecgo when a fakecgo-created OS thread exits and runtime.mstart returns back into threadentry with BP clobbered, which can break the frame-pointer epilogue on newer Go toolchains.

Changes:

  • Routes threadentry’s call to ts.fn (runtime.mstart) through a new amd64 assembly shim that saves/restores callee-saved registers (notably BP) around the call.
  • Adds Go declarations / non-amd64 fallback for callThreadEntryFn.
  • Adds a regression test that forces locked OS threads to exit to exercise the mstart return path.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/fakecgo/trampolines_amd64.s Adds callThreadEntryFn asm shim to preserve callee-saved regs (incl. frame pointer) across mstart.
internal/fakecgo/threadentry_repro_test.go Adds regression test that reproduces the prior crash by forcing locked threads to exit.
internal/fakecgo/threadentry_noasm.go Adds non-amd64 Go fallback implementation for callThreadEntryFn.
internal/fakecgo/threadentry_amd64.go Adds amd64 Go declaration for the asm-implemented callThreadEntryFn.
internal/fakecgo/go_linux.go Updates threadentry to call callThreadEntryFn(ts.fn) instead of directly invoking the raw PC.
internal/fakecgo/go_darwin.go Same threadentry update for Darwin.
internal/fakecgo/go_freebsd.go Same threadentry update for FreeBSD.
internal/fakecgo/go_netbsd.go Same threadentry update for NetBSD.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/fakecgo/threadentry_amd64.go
Comment thread internal/fakecgo/threadentry_test.go Outdated
Comment thread internal/fakecgo/threadentry_test.go Outdated
@gdams
gdams requested a review from hajimehoshi July 31, 2026 12:29
Comment thread internal/fakecgo/threadentry_test.go Outdated
@gdams
gdams force-pushed the fix/fakecgo-threadentry-framepointer branch from b9d7cba to 56d2923 Compare July 31, 2026 12:37
@gdams
gdams requested a review from TotallyGamerJet July 31, 2026 12:37

@TotallyGamerJet TotallyGamerJet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@gdams
gdams force-pushed the fix/fakecgo-threadentry-framepointer branch from 56d2923 to a197cd4 Compare July 31, 2026 12:44

@hajimehoshi hajimehoshi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM % comments, thanks!

Comment thread internal/fakecgo/go_darwin.go Outdated
Comment thread internal/fakecgo/go_freebsd.go Outdated
Comment thread internal/fakecgo/go_linux.go Outdated
Comment thread internal/fakecgo/go_netbsd.go Outdated
@gdams

gdams commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@hajimehoshi updated PTAL

@hajimehoshi hajimehoshi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still LGTM

@hajimehoshi
hajimehoshi merged commit 077e9d5 into ebitengine:main Jul 31, 2026
25 checks passed
hajimehoshi pushed a commit that referenced this pull request Jul 31, 2026
…entry (#486)

threadentry calls runtime.mstart directly. For a fakecgo thread the
M's stack is system-allocated, so when the M exits mexit(osStack=true)
returns from mstart back into threadentry. mstart returns with BP
clobbered (0); on amd64 the frame-pointer LEAVE epilogue (MOV BP,SP;
POP BP) then dereferences a NULL pointer and crashes.

Route the mstart call through an assembly shim that saves and restores
the callee-saved registers (including BP) on the stack, mirroring
what real cgo's crosscall_amd64 does. The existing call5 helper
cannot be reused because it stashes SP in BP, which mstart zeroes.

Add a regression test that forces locked OS threads to exit.

Closes #485
@gdams
gdams deleted the fix/fakecgo-threadentry-framepointer branch July 31, 2026 13:25
hajimehoshi added a commit that referenced this pull request Jul 31, 2026
…n threadentry (#486)"

This reverts commit ff7de0f.

Reason: TestThreadEntryReturn caused SEGV on Linux Arm w/ Go 1.25
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.

internal/fakecgo: threadentry SIGSEGVs in its LEAVE epilogue when a fakecgo-created OS thread exits (amd64, Go 1.28 frame-pointer epilogue)

4 participants