Commit e99ffe10 authored by cznic's avatar cznic
Browse files

linux: let abort() die by SIGABRT with the kernel's default disposition again, updates #53

Since 0ae842f8 (v1.76.0) ___libc_sigaction is a Go function that records
dispositions for delivery through os/signal and never touches the kernel;
Xabort's ___libc_sigaction(SIGABRT, SIG_DFL) therefore leaves the Go
runtime's SIGABRT handler installed, and the kill(getpid(), SIGABRT) that
follows prints "SIGABRT: abort" and a goroutine dump to stderr before the
runtime re-raises the signal and dies. In v1.75.7 the transpiled musl
__libc_sigaction issued the rt_sigaction syscall, so the process died
silently, as a C abort() does.

The difference is observable: Tcl's exec reports a child that dies by a
signal as "child killed: SIGABRT" only when its stderr stayed empty, and
sqlite's writecrash.test (test_devsym.c calls abort() on the n-th write)
matches on that, so every linux target of modernc.org/libsqlite3 fails
writecrash-1.1.1 with v1.76.0 and passes with v1.75.7 - verified A/B on
linux/amd64 with everything else equal.

resetSigDfl (abort_linux.go, the musl linux ports) installs SIG_DFL with
rt_sigaction and unblocks the signal with rt_sigprocmask, replacing the
runtime's handler for that one signal; Xabort calls it after the
bookkeeping call to ___libc_sigaction and then raises SIGABRT
thread-directed with tgkill, as the Go runtime's raise() and the netbsd
Xabort do, because a process-directed kill(2) is asynchronous and the
calling thread can run on before the signal lands. The kill and the panic
that follow are unreachable fallbacks. With this writecrash.test passes
again (0 errors out of 997 tests), and libsqlite3's full Tcl permutation
on linux/amd64 is back to its waived failures only. Builds on all eight
linux ports.

Co-Authored-By: default avatarClaude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dq5kdSFJ1eyaaH2cjpMAKk
parent f63e9424
Loading
Loading
Loading
Loading

abort_linux.go

0 → 100644
+42 −0
Changes for abort_linux.go: 42 added lines, 0 removed lines.
Original line number Diff line number Diff line
// Copyright 2026 The Libc Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build linux && (amd64 || arm64 || loong64 || ppc64le || s390x || riscv64 || 386 || arm)

package libc // import "modernc.org/libc"

import (
	"unsafe"

	"golang.org/x/sys/unix"
)

// kernelSigaction is the kernel's struct sigaction as rt_sigaction takes it
// (arch/*/include/uapi/asm/signal.h): sa_handler, sa_flags, sa_restorer and
// the kernel sigset_t, which is _NSIG/8 = 8 bytes on every port built here
// (mips, where it is 16, is not one of them). sa_flags is an unsigned long,
// hence uintptr.
type kernelSigaction struct {
	handler  uintptr
	flags    uintptr
	restorer uintptr
	mask     uint64
}

// resetSigDfl installs the kernel's default disposition for sig and unblocks
// it in the calling thread, replacing whatever handler the kernel has for it,
// the Go runtime's included. ___libc_sigaction cannot do that: it only records
// dispositions for delivery through os/signal (issue #53), and the runtime
// keeps its own handler installed even after signal.Reset. It is for the one
// place that must die by a signal rather than handle it, Xabort: with the
// runtime's SIGABRT handler in place the process prints a goroutine dump to
// stderr before it dies, which no C abort() does. A parent that examines the
// child's death, such as Tcl's exec ("child killed: SIGABRT" only when stderr
// stayed empty), tells the two apart.
func resetSigDfl(sig int32) {
	sa := kernelSigaction{handler: SIG_DFL}
	unix.RawSyscall6(unix.SYS_RT_SIGACTION, uintptr(sig), uintptr(unsafe.Pointer(&sa)), 0, unsafe.Sizeof(sa.mask), 0, 0)
	set := uint64(1) << (uint(sig) - 1)
	unix.RawSyscall6(unix.SYS_RT_SIGPROCMASK, SIG_UNBLOCK, uintptr(unsafe.Pointer(&set)), 0, unsafe.Sizeof(set), 0, 0)
}
+14 −0
Changes for libc_musl.go: 14 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -654,8 +654,22 @@ func _exit(tls *TLS, code int32) {

var abort Tsigaction

// abort dies by SIGABRT with the signal's default disposition, as musl's
// does. ___libc_sigaction records the SIG_DFL for os/signal only and leaves
// the Go runtime's SIGABRT handler in the kernel, which would print a
// goroutine dump to stderr and only then let the process die; resetSigDfl
// puts the kernel's default back so that the death is silent, with the core
// dump the resource limits allow, and a parent sees a child killed by SIGABRT
// with an empty stderr. sqlite's writecrash.test checks exactly that. The
// signal is sent thread-directed with tgkill, as the Go runtime's raise()
// does and as the netbsd Xabort explains: a process-directed kill(2) is
// asynchronous and the calling thread can run on before it lands. With
// SIG_DFL installed the kernel terminates the process in tgkill; the
// process-directed kill and the panic are unreachable fallbacks.
func Xabort(tls *TLS) {
	___libc_sigaction(tls, SIGABRT, uintptr(unsafe.Pointer(&abort)), 0)
	resetSigDfl(SIGABRT)
	unix.Tgkill(unix.Getpid(), unix.Gettid(), unix.Signal(SIGABRT))
	unix.Kill(unix.Getpid(), unix.Signal(SIGABRT))
	panic(todo("unrechable"))
}