summaryrefslogtreecommitdiff
path: root/cmd/ioworkload/scenario_signals.go
blob: bb440aca070341f06841e7dbe8c7243d2875fc9e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
package main

import (
	"fmt"
	"runtime"
	"syscall"
	"unsafe"

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

// sigWaitTimeoutNsec is the rt_sigtimedwait timeout: a short 100ms so the
// scenario can never hang even if no signal is pending (e.g. if the Go runtime
// somehow consumed it first). We always send the signal to ourselves first, so
// the wait normally returns immediately.
const sigWaitTimeoutNsec = 100 * 1000 * 1000

// kernelSigsetWords is the number of 64-bit words in the kernel sigset_t that
// the rt_sig* syscalls expect. The kernel ABI uses _NSIG (64) bits, i.e. one
// 8-byte word on every Linux architecture ior targets. The rt_sig* syscalls
// take the sigset size in bytes (kernelSigsetWords*8) as their final argument.
const kernelSigsetWords = 1

// sigSetBytes is the sigset size argument the rt_sig* syscalls expect.
const sigSetBytes = kernelSigsetWords * 8

// Kernel signal-disposition and sigaltstack constants. The golang.org/x/sys
// unix package does not export these for our target version, and the kernel
// values are stable ABI, so we define them locally.
const (
	sigIgn     = 1    // SIG_IGN
	ssDisable  = 2    // SS_DISABLE
	minStackSz = 8192 // generous MINSIGSTKSZ; only needs to be valid
	siQueue    = -1   // SI_QUEUE: user-generated siginfo (negative si_code)
)

// kernelSigset is the sigset_t as the rt_sig* syscalls expect it (a fixed
// _NSIG-bit mask), distinct from the wider userspace sigset_t. We pass its
// address plus sigSetBytes to the raw syscalls.
type kernelSigset [kernelSigsetWords]uint64

func (s *kernelSigset) add(sig int) {
	// Signals are 1-based; set bit (sig-1) within the mask.
	s[(sig-1)/64] |= 1 << (uint(sig-1) % 64)
}

// kernelSigaction mirrors struct kernel_sigaction on x86_64: handler, flags,
// restorer, then the blocked-during-handler mask. We never let the handler run
// (the signal stays blocked), so the restorer is unused for SIG_IGN/SIG_DFL.
type kernelSigaction struct {
	Handler  uintptr
	Flags    uint64
	Restorer uintptr
	Mask     kernelSigset
}

// signalsBasic exercises the SIGNALS syscall family entirely self-directed, so
// it mutates no other process. The flow is made deterministic and hang-proof:
//
//   - rt_sigaction installs an ignore disposition (SIG_IGN) for SIGUSR1.
//   - rt_sigprocmask BLOCKS SIGUSR1 before we ever send it, so delivering the
//     signal to ourselves leaves it PENDING instead of running a handler or
//     killing the process.
//   - sigaltstack sets (and then disables) an alternate signal stack.
//   - kill / tgkill / tkill / rt_sigqueueinfo each send SIGUSR1 to this very
//     process/thread; because the signal is blocked these just mark it pending.
//   - rt_sigpending confirms the pending signal can be queried.
//   - rt_sigtimedwait reaps the pending SIGUSR1 with a SHORT 100ms timeout so
//     the call cannot block indefinitely.
//
// Finally the original signal mask and SIGUSR1 disposition are restored and the
// alternate stack is disabled — we leave the process as we found it.
//
// We issue the raw syscalls directly (rather than relying on the Go runtime's
// signal machinery) so the enter_/exit_ tracepoints FIRE regardless of how the
// Go runtime multiplexes its own handlers. The goal is that the syscalls are
// issued and traced, not that a user handler necessarily runs.
//
// LockOSThread pins this goroutine to one OS thread so the tid we read with
// gettid() is the tid tgkill/tkill target, and so the per-thread blocked mask
// applies to the same thread that issues the wait.
func signalsBasic() error {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	const sig = int(unix.SIGUSR1)

	oldAct, err := installIgnoreHandler(sig)
	if err != nil {
		return err
	}
	defer restoreHandler(sig, oldAct)

	oldMask, err := blockSignal(sig)
	if err != nil {
		return err
	}
	defer restoreSignalMask(oldMask)

	if err := exerciseAltStack(); err != nil {
		return err
	}

	tid := unix.Gettid()
	pid := unix.Getpid()
	if err := sendSelfSignals(pid, tid, sig); err != nil {
		return err
	}

	if err := queryPending(); err != nil {
		return err
	}

	return reapSignal(sig)
}

// installIgnoreHandler sets SIGUSR1 to SIG_IGN via rt_sigaction and returns the
// previous disposition so it can be restored. SIG_IGN needs no restorer because
// the signal stays blocked and the disposition is never actually invoked.
func installIgnoreHandler(sig int) (kernelSigaction, error) {
	newAct := kernelSigaction{Handler: sigIgn}
	var oldAct kernelSigaction
	if _, _, errno := syscall.RawSyscall6(
		unix.SYS_RT_SIGACTION,
		uintptr(sig),
		uintptr(unsafe.Pointer(&newAct)),
		uintptr(unsafe.Pointer(&oldAct)),
		sigSetBytes,
		0, 0,
	); errno != 0 {
		return oldAct, fmt.Errorf("rt_sigaction install: %w", errno)
	}
	return oldAct, nil
}

// restoreHandler reinstalls a previously saved SIGUSR1 disposition; best-effort
// cleanup so we leave the process as we found it. If the saved action carried a
// real handler+restorer (e.g. the Go runtime's), restoring it verbatim is the
// correct behaviour.
func restoreHandler(sig int, old kernelSigaction) {
	_, _, _ = syscall.RawSyscall6(
		unix.SYS_RT_SIGACTION,
		uintptr(sig),
		uintptr(unsafe.Pointer(&old)),
		0,
		sigSetBytes,
		0, 0,
	)
}

// blockSignal adds sig to the thread's blocked set via rt_sigprocmask and
// returns the previous mask so the caller can restore it.
func blockSignal(sig int) (kernelSigset, error) {
	var newSet, oldSet kernelSigset
	newSet.add(sig)
	if _, _, errno := syscall.RawSyscall6(
		unix.SYS_RT_SIGPROCMASK,
		uintptr(unix.SIG_BLOCK),
		uintptr(unsafe.Pointer(&newSet)),
		uintptr(unsafe.Pointer(&oldSet)),
		sigSetBytes,
		0, 0,
	); errno != 0 {
		return oldSet, fmt.Errorf("rt_sigprocmask block: %w", errno)
	}
	return oldSet, nil
}

// restoreSignalMask reinstalls the previously saved blocked set; best-effort.
func restoreSignalMask(old kernelSigset) {
	_, _, _ = syscall.RawSyscall6(
		unix.SYS_RT_SIGPROCMASK,
		uintptr(unix.SIG_SETMASK),
		uintptr(unsafe.Pointer(&old)),
		0,
		sigSetBytes,
		0, 0,
	)
}

// sigaltstackT mirrors struct sigaltstack (stack_t): pointer, flags, size.
type sigaltstackT struct {
	Sp    uintptr
	Flags int32
	_     int32 // padding to 8-byte alignment for Size
	Size  uint64
}

// exerciseAltStack installs a temporary alternate signal stack via sigaltstack
// and then disables it again, so the sigaltstack tracepoint fires. The stack is
// never actually used (the signal stays blocked); it just has to be valid.
func exerciseAltStack() error {
	stackMem := make([]byte, minStackSz)
	newStack := sigaltstackT{
		Sp:    uintptr(unsafe.Pointer(&stackMem[0])),
		Flags: 0,
		Size:  uint64(len(stackMem)),
	}
	if _, _, errno := syscall.RawSyscall(
		unix.SYS_SIGALTSTACK,
		uintptr(unsafe.Pointer(&newStack)),
		0, 0,
	); errno != 0 {
		return fmt.Errorf("sigaltstack set: %w", errno)
	}
	runtime.KeepAlive(stackMem)
	// Disable the alternate stack again to leave the thread as we found it.
	disable := sigaltstackT{Flags: ssDisable}
	_, _, _ = syscall.RawSyscall(
		unix.SYS_SIGALTSTACK,
		uintptr(unsafe.Pointer(&disable)),
		0, 0,
	)
	return nil
}

// siginfoQueue is a minimal siginfo_t for rt_sigqueueinfo. The kernel copies a
// fixed 128-byte buffer, so we pad the tail to that canonical size.
type siginfoQueue struct {
	Signo int32
	Errno int32
	Code  int32
	_     [116]byte
}

// sendSelfSignals sends SIGUSR1 to this process/thread four different ways:
// kill (process), tgkill (thread group + tid), tkill (tid), and
// rt_sigqueueinfo (process, with a siginfo payload). Because SIGUSR1 is blocked
// these only mark the signal pending; they never run a handler or kill us.
func sendSelfSignals(pid, tid, sig int) error {
	if err := unix.Kill(pid, syscall.Signal(sig)); err != nil {
		return fmt.Errorf("kill self: %w", err)
	}
	if _, _, errno := syscall.RawSyscall(
		unix.SYS_TGKILL, uintptr(pid), uintptr(tid), uintptr(sig),
	); errno != 0 {
		return fmt.Errorf("tgkill self: %w", errno)
	}
	if _, _, errno := syscall.RawSyscall(
		unix.SYS_TKILL, uintptr(tid), uintptr(sig), 0,
	); errno != 0 {
		return fmt.Errorf("tkill self: %w", errno)
	}
	info := siginfoQueue{Signo: int32(sig), Code: siQueue}
	if _, _, errno := syscall.RawSyscall(
		unix.SYS_RT_SIGQUEUEINFO,
		uintptr(pid), uintptr(sig), uintptr(unsafe.Pointer(&info)),
	); errno != 0 {
		return fmt.Errorf("rt_sigqueueinfo self: %w", errno)
	}
	return nil
}

// queryPending issues rt_sigpending so its tracepoint fires; the result is not
// inspected (the wait below is what actually reaps the signal).
func queryPending() error {
	var pending kernelSigset
	if _, _, errno := syscall.RawSyscall(
		unix.SYS_RT_SIGPENDING,
		uintptr(unsafe.Pointer(&pending)),
		sigSetBytes,
		0,
	); errno != 0 {
		return fmt.Errorf("rt_sigpending: %w", errno)
	}
	return nil
}

// reapSignal waits for the (blocked, pending) SIGUSR1 via rt_sigtimedwait with
// a short 100ms timeout. Since we already queued SIGUSR1 to ourselves, the call
// returns immediately; the timeout exists purely as a hang guard. EAGAIN (timed
// out without a pending signal) is tolerated so the scenario never fails just
// because the Go runtime consumed the pending signal first.
func reapSignal(sig int) error {
	var waitSet kernelSigset
	waitSet.add(sig)
	timeout := unix.Timespec{Sec: 0, Nsec: sigWaitTimeoutNsec}
	var info siginfoQueue
	_, _, errno := syscall.RawSyscall6(
		unix.SYS_RT_SIGTIMEDWAIT,
		uintptr(unsafe.Pointer(&waitSet)),
		uintptr(unsafe.Pointer(&info)),
		uintptr(unsafe.Pointer(&timeout)),
		sigSetBytes,
		0, 0,
	)
	if errno != 0 && errno != syscall.EAGAIN {
		return fmt.Errorf("rt_sigtimedwait: %w", errno)
	}
	return nil
}