-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbackend_linux_amd64.go
More file actions
640 lines (584 loc) · 21.2 KB
/
Copy pathbackend_linux_amd64.go
File metadata and controls
640 lines (584 loc) · 21.2 KB
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
//go:build linux && amd64
package debugger
import (
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"sync"
"syscall"
"golang.org/x/sys/unix"
)
func newBackend() Backend {
return &linuxBackend{tracer: newTracerThread()}
}
// tracerThread pins a single OS thread and runs every ptrace(2) control op on
// it. On Linux ptrace is thread-bound: after a tracee is attached (via
// PTRACE_TRACEME during fork, or PTRACE_ATTACH), *all* subsequent ptrace
// requests for it must come from the exact thread that became the tracer, or
// they fail with ESRCH. bingo previously issued control ops from two different
// goroutines/threads (the engine loop and each waitLoop), so ops made from the
// wait thread hit ESRCH and were silently swallowed, wedging step-over. This
// mirrors Delve's execPtraceFunc / ptraceThread (pkg/proc/native/proc.go):
// funnel fork/exec, attach, detach, cont, single-step, get/set-regs, peek/poke
// and set-options through one thread. wait4 is NOT routed here — it is safe
// from any thread of the tracer process (Delve calls sys.Wait4 directly), and
// keeping it off-thread lets the engine issue control ops while a wait is
// outstanding.
type tracerThread struct {
funcCh chan func()
doneCh chan struct{}
quit chan struct{}
once sync.Once
}
func newTracerThread() *tracerThread {
t := &tracerThread{
funcCh: make(chan func()),
doneCh: make(chan struct{}),
quit: make(chan struct{}),
}
go t.run()
return t
}
func (t *tracerThread) run() {
runtime.LockOSThread()
// Stays welded to its OS thread until quit, so the kernel keeps seeing the
// same tracer. Returning ends the goroutine and releases the locked thread.
for {
select {
case fn := <-t.funcCh:
fn()
t.doneCh <- struct{}{}
case <-t.quit:
return
}
}
}
// execPtrace runs fn on the dedicated tracer thread and blocks until it
// completes. Concurrent callers (engine loop vs waitLoop) are serialised by the
// unbuffered channels, so ptrace ops never interleave. After close() the op
// becomes a no-op rather than blocking forever (the tracee is gone anyway).
func (t *tracerThread) execPtrace(fn func()) {
select {
case t.funcCh <- fn:
<-t.doneCh
case <-t.quit:
}
}
// close stops the tracer goroutine so its locked OS thread is released. Only
// funcCh's sole channel of control (quit) is closed — never funcCh itself — so
// a racing execPtrace can never send on a closed channel. Callers must ensure
// no execPtrace is in flight (the engine calls this only after its loop exits).
func (t *tracerThread) close() {
t.once.Do(func() { close(t.quit) })
}
// tracerExecer is implemented by backends whose ptrace control ops must all run
// on one dedicated thread. Only the linux backend implements it; the platform
// free functions (startTracedProcess/attachToProcess/killProcess) use it to run
// the fork/attach/detach on that thread. Darwin does not implement it.
type tracerExecer interface {
execPtrace(fn func())
}
type linuxBackend struct {
pid int
stepping bool // true after SingleStep; classifies the next SIGTRAP
stepTID int // the exact thread SingleStep was issued against
lastStopTID int
tracer *tracerThread
}
func (b *linuxBackend) execPtrace(fn func()) { b.tracer.execPtrace(fn) }
// closeTracer releases the dedicated tracer thread. The engine calls this after
// its loop exits (process gone), when no further ptrace ops can be issued.
func (b *linuxBackend) closeTracer() { b.tracer.close() }
const linuxPtraceOptions = syscall.PTRACE_O_TRACEEXIT |
syscall.PTRACE_O_TRACEEXEC |
syscall.PTRACE_O_TRACECLONE
// startTracedProcess forks under ptrace. The child is stopped at its first
// instruction (execve SIGTRAP) ready for the engine to set breakpoints. The
// fork+exec, the reap of the initial execve stop and PTRACE_SETOPTIONS all run
// on the backend's dedicated tracer thread: the forking thread becomes the
// tracee's tracer, so every later ptrace op must originate from that same
// thread.
func startTracedProcess(b Backend, binaryPath string, args []string, env []string) (int, *exec.Cmd, error) {
tracer, ok := b.(tracerExecer)
if !ok {
return 0, nil, fmt.Errorf("startTracedProcess: backend does not support a tracer thread")
}
// codeql-suppress[go/command-injection]: The debugger intentionally launches the local binary selected by the operator.
cmd := exec.Command(binaryPath, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}
if len(env) > 0 {
cmd.Env = append(os.Environ(), env...)
}
var startErr error
tracer.execPtrace(func() {
if err := cmd.Start(); err != nil {
startErr = fmt.Errorf("exec %q: %w", binaryPath, err)
return
}
pid := cmd.Process.Pid
var ws syscall.WaitStatus
if _, err := syscall.Wait4(pid, &ws, 0, nil); err != nil {
_ = cmd.Process.Kill()
startErr = fmt.Errorf("wait for execve stop: %w", err)
return
}
if !ws.Stopped() || ws.StopSignal() != syscall.SIGTRAP {
_ = cmd.Process.Kill()
startErr = fmt.Errorf("unexpected initial stop: %v", ws)
return
}
if err := syscall.PtraceSetOptions(pid, linuxPtraceOptions); err != nil {
_ = cmd.Process.Kill()
startErr = fmt.Errorf("PTRACE_SETOPTIONS: %w", err)
return
}
})
if startErr != nil {
return 0, nil, startErr
}
return cmd.Process.Pid, cmd, nil
}
func attachToProcess(b Backend, pid int) error {
tracer, ok := b.(tracerExecer)
if !ok {
return fmt.Errorf("attachToProcess: backend does not support a tracer thread")
}
var attachErr error
tracer.execPtrace(func() {
if err := syscall.PtraceAttach(pid); err != nil {
attachErr = fmt.Errorf("PTRACE_ATTACH pid %d: %w", pid, err)
return
}
var ws syscall.WaitStatus
if _, err := syscall.Wait4(pid, &ws, 0, nil); err != nil {
attachErr = fmt.Errorf("wait after PTRACE_ATTACH: %w", err)
return
}
})
return attachErr
}
// killProcess terminates a launched tracee (SIGKILL) or detaches from an
// attached one. running reports whether the engine's waitLoop is in flight —
// true for a running tracee, false for one suspended at a stop.
func killProcess(b Backend, pid int, cmd *exec.Cmd, running bool) error {
if cmd != nil {
// SIGKILL via the OS handle is not a ptrace op, so it is safe from any
// thread and keeps Kill responsive even if the tracer thread is busy.
if err := cmd.Process.Kill(); err != nil && !isAlreadyExited(err) {
return err
}
// Reaping the zombie belongs to the waitLoop whenever one is in flight
// (a running tracee). It is blocked in Wait4(-1, WALL) and will absorb
// every thread's SIGKILL death and surface StopKilled. A second reaper
// here would both (a) race that waitLoop for the same stops and (b) —
// since a Go tracee is always multi-threaded — never make progress:
// Wait4(pid) targets only the thread-group leader, whose zombie is not
// reapable until all siblings are, and killProcess cannot reach the
// siblings. Either way Kill wedges (the deadlock reported in #111). So
// only reap here when there is NO waitLoop — the tracee was suspended at
// a stop, making killProcess the sole reaper.
if !running {
if lb, ok := b.(*linuxBackend); ok {
lb.reapAfterKill()
}
}
return nil
}
// Attached (not launched): detach, don't kill — we don't own the process.
// PTRACE_DETACH must run on the tracer thread.
if tracer, ok := b.(tracerExecer); ok {
tracer.execPtrace(func() { _ = syscall.PtraceDetach(pid) })
} else {
_ = syscall.PtraceDetach(pid)
}
return nil
}
// reapAfterKill drains a SIGKILL'd tracee that has no waitLoop to reap it (it
// was suspended at a ptrace stop when killed). It waits on Wait4(-1) — any
// thread — never Wait4(pid): a Go tracee is always multi-threaded and the
// thread-group leader's zombie stays unreapable until every sibling thread is
// reaped, so waiting on the leader's pid alone blocks forever. A thread frozen
// at a ptrace stop (e.g. the breakpoint we were suspended at) will not proceed
// to death until resumed, so continue any stopped thread before waiting again;
// the process-wide SIGKILL then kills it. Returns once ECHILD reports the whole
// thread group gone.
//
// It must never run concurrently with the engine's waitLoop: two Wait4(-1)
// callers would steal each other's stops. killProcess guarantees that by only
// invoking it when the tracee was not running (no waitLoop in flight).
func (b *linuxBackend) reapAfterKill() {
for {
var ws syscall.WaitStatus
wpid, err := syscall.Wait4(-1, &ws, syscall.WALL, nil)
switch {
case err == nil:
if ws.Stopped() {
_ = b.continueIfTraceeExists(wpid, 0)
}
// Exited/Signaled: that thread is reaped; loop for the rest.
case isNoChildProcess(err):
return // whole thread group reaped
case errors.Is(err, syscall.EINTR):
// interrupted by a signal; retry
default:
return // unexpected wait4 error: nothing left to reap
}
}
}
func isAlreadyExited(err error) bool {
return err != nil && err.Error() == "os: process already finished"
}
func (b *linuxBackend) ContinueProcess() error {
b.stepping = false
b.stepTID = 0
tid := b.traceTID()
var err error
b.execPtrace(func() { err = syscall.PtraceCont(tid, 0) })
if err != nil {
return fmt.Errorf("PTRACE_CONT tid %d: %w", tid, err)
}
return nil
}
func (b *linuxBackend) SingleStep(tid int) error {
b.stepping = true
b.stepTID = tid
var err error
b.execPtrace(func() { err = syscall.PtraceSingleStep(tid) })
if err != nil {
return fmt.Errorf("PTRACE_SINGLESTEP tid %d: %w", tid, err)
}
return nil
}
// StopProcess asynchronously interrupts the running tracee for Pause. It
// directs SIGSTOP at the MAIN thread specifically (tgkill(pid, pid, SIGSTOP))
// rather than the whole thread group (kill(pid, ...)). A process-directed
// SIGSTOP may be dequeued by any thread, and Wait() deliberately swallows a
// non-main thread's SIGSTOP as a clone group-stop (the sig==SIGSTOP &&
// tid!=b.pid branch), so on a multithreaded target a group-directed Pause
// could be lost. Targeting the main thread (whose TID equals the tgid) makes
// the signal surface from Wait() as StopEvent{StopSignal, SIGSTOP} with
// TID==b.pid, where the engine's manual-stop detection turns it into
// EventPaused. The engine never injects this SIGSTOP back (Continue resumes
// with signal 0), so it triggers no group-stop and resume is a plain
// ContinueProcess. ESRCH (thread already gone) is an idempotent no-op,
// matching process.kill. tgkill is a plain signal syscall,
// not a ptrace op, so it need not run on the tracer thread.
func (b *linuxBackend) StopProcess() error {
if b.pid == 0 {
return fmt.Errorf("StopProcess: no process")
}
if err := syscall.Tgkill(b.pid, b.pid, syscall.SIGSTOP); err != nil && err != syscall.ESRCH {
return fmt.Errorf("StopProcess: %w", err)
}
return nil
}
// PauseSignal is SIGSTOP: the signal StopProcess directs at the main thread and
// that the engine turns into EventPaused. See Backend.PauseSignal.
func (b *linuxBackend) PauseSignal() int { return int(syscall.SIGSTOP) }
// ReadMemory bulk-copies the tracee's address space. process_vm_readv(2) is the
// fast path: a single syscall for the whole buffer that — unlike ptrace(2) — is
// NOT thread-bound, so it runs directly off the calling goroutine without the
// tracer-thread handoff and never word-at-a-times. This is what keeps the
// goroutine snapshot (dozens of small reads per stop, across every live
// goroutine) cheap; the old PTRACE_PEEKDATA-through-execPtrace path was orders
// of magnitude slower and pushed the churn e2e past its target's watchdog.
// PTRACE_PEEKDATA remains the fallback for the rare case process_vm_readv is
// unavailable (old kernel) or short-reads.
func (b *linuxBackend) ReadMemory(addr uint64, dst []byte) error {
if len(dst) == 0 {
return nil
}
if b.pid > 0 {
local := []unix.Iovec{{Base: &dst[0], Len: uint64(len(dst))}}
remote := []unix.RemoteIovec{{Base: uintptr(addr), Len: len(dst)}}
if n, err := unix.ProcessVMReadv(b.pid, local, remote, 0); err == nil && n == len(dst) {
return nil
}
}
tid := b.traceTID()
var n int
var err error
b.execPtrace(func() { n, err = syscall.PtracePeekData(tid, uintptr(addr), dst) })
if err != nil {
return fmt.Errorf("PTRACE_PEEKDATA tid %d 0x%x: %w", tid, addr, err)
}
if n != len(dst) {
return fmt.Errorf("PTRACE_PEEKDATA tid %d 0x%x: short read %d/%d", tid, addr, n, len(dst))
}
return nil
}
func (b *linuxBackend) WriteMemory(addr uint64, src []byte) error {
tid := b.traceTID()
var n int
var err error
b.execPtrace(func() { n, err = syscall.PtracePokeData(tid, uintptr(addr), src) })
if err != nil {
return fmt.Errorf("PTRACE_POKEDATA tid %d 0x%x: %w", tid, addr, err)
}
if n != len(src) {
return fmt.Errorf("PTRACE_POKEDATA tid %d 0x%x: short write %d/%d", tid, addr, n, len(src))
}
return nil
}
// GetRegisters reads PTRACE_GETREGS. The Go runtime stores g at FS_BASE on amd64.
func (b *linuxBackend) GetRegisters(tid int) (Registers, error) {
var r syscall.PtraceRegs
var err error
b.execPtrace(func() { err = syscall.PtraceGetRegs(tid, &r) })
if err != nil {
return Registers{}, fmt.Errorf("PTRACE_GETREGS tid %d: %w", tid, err)
}
return Registers{
PC: r.Rip,
SP: r.Rsp,
BP: r.Rbp,
TLS: r.Fs_base,
}, nil
}
// SetRegisters writes back the engine-owned fields, preserving everything else
// by reading the full register set first.
func (b *linuxBackend) SetRegisters(tid int, reg Registers) error {
var r syscall.PtraceRegs
var getErr, setErr error
b.execPtrace(func() {
if getErr = syscall.PtraceGetRegs(tid, &r); getErr != nil {
return
}
r.Rip = reg.PC
r.Rsp = reg.SP
r.Rbp = reg.BP
r.Fs_base = reg.TLS
setErr = syscall.PtraceSetRegs(tid, &r)
})
if getErr != nil {
return fmt.Errorf("PTRACE_GETREGS (pre-set) tid %d: %w", tid, getErr)
}
if setErr != nil {
return fmt.Errorf("PTRACE_SETREGS tid %d: %w", tid, setErr)
}
return nil
}
func (b *linuxBackend) Threads() ([]int, error) {
entries, err := os.ReadDir(fmt.Sprintf("/proc/%d/task", b.pid))
if err != nil {
return nil, fmt.Errorf("read /proc/%d/task: %w", b.pid, err)
}
tids := make([]int, 0, len(entries))
for _, e := range entries {
var tid int
if _, err := fmt.Sscanf(e.Name(), "%d", &tid); err == nil {
tids = append(tids, tid)
}
}
if len(tids) == 0 {
return nil, fmt.Errorf("no threads for pid %d", b.pid)
}
return tids, nil
}
// Wait blocks until the tracee produces a meaningful debug stop. Single-step
// vs breakpoint disambiguation uses b.stepping AND b.stepTID: only a cause==0
// SIGTRAP on the exact thread we stepped is the step's completion; the same
// stop on any other thread is that thread hitting a software breakpoint.
// PTRACE_EVENT stops (clone/exec/exit) are handled internally and don't
// surface to the engine.
//
// wait4 runs on the calling (waitLoop) thread, NOT the tracer thread: waiting
// for a tracee is legal from any thread of the tracer process, and keeping it
// off the tracer thread lets the engine issue control ops concurrently. Every
// ptrace CONTROL op below, however, is funnelled through b.execPtrace so it
// executes on the one thread the kernel accepts ptrace requests from.
//
//nolint:gocognit,gocyclo // The wait loop is one serialized ptrace state machine.
func (b *linuxBackend) Wait() (StopEvent, error) {
for {
var ws syscall.WaitStatus
// WALL includes clone()d threads.
tid, err := syscall.Wait4(-1, &ws, syscall.WALL, nil)
if err != nil {
if isNoChildProcess(err) {
return StopEvent{Reason: StopExited, TID: b.pid}, nil
}
return StopEvent{}, fmt.Errorf("wait4: %w", err)
}
if ws.Exited() {
if tid == b.pid {
b.recordStop(tid)
return StopEvent{Reason: StopExited, TID: tid, ExitCode: ws.ExitStatus()}, nil
}
continue
}
if ws.Signaled() {
if tid != b.pid {
continue
}
b.recordStop(tid)
return StopEvent{Reason: StopKilled, TID: tid}, nil
}
if !ws.Stopped() {
continue
}
sig := ws.StopSignal()
// PTRACE_EVENT stops are encoded as SIGTRAP | (event << 8).
if sig == syscall.SIGTRAP {
cause := ws.TrapCause()
switch cause {
case syscall.PTRACE_EVENT_CLONE:
if err := b.continueIfTraceeExists(tid, 0); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT clone parent tid %d: %w", tid, err)
}
continue
case syscall.PTRACE_EVENT_EXIT:
if tid != b.pid {
if err := b.continueIfTraceeExists(tid, 0); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT exiting thread tid %d: %w", tid, err)
}
continue
}
// Main thread is about to exit. PTRACE_O_TRACEEXIT stops it here
// BEFORE it dies, and the engine tears down on this StopExited, so
// the real status never resurfaces as a later wait4 Exited()/
// Signaled(). Read it now via PTRACE_GETEVENTMSG (a wait(2)-encoded
// status) so a non-zero exit or a fatal signal isn't misreported as
// a clean exit 0. GETEVENTMSG must run before we resume the thread —
// once continued it is gone and the message is unreadable.
var msg uint
var msgErr error
b.execPtrace(func() { msg, msgErr = syscall.PtraceGetEventMsg(tid) })
if err := b.continueIfTraceeExists(tid, 0); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT exiting process tid %d: %w", tid, err)
}
b.recordStop(tid)
if msgErr == nil {
status := syscall.WaitStatus(msg)
switch {
case status.Signaled():
return StopEvent{Reason: StopKilled, TID: tid}, nil
case status.Exited():
return StopEvent{Reason: StopExited, TID: tid, ExitCode: status.ExitStatus()}, nil
}
}
// Status unreadable (e.g. ESRCH racing a Kill) or unexpected shape:
// fall back to a clean exit rather than inventing a code.
return StopEvent{Reason: StopExited, TID: tid, ExitCode: 0}, nil
case syscall.PTRACE_EVENT_EXEC:
if err := b.continueIfTraceeExists(tid, 0); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT exec tid %d: %w", tid, err)
}
continue
case 0:
b.recordStop(tid)
// Only the exact thread we single-stepped produces a
// single-step SIGTRAP. A cause==0 SIGTRAP on any OTHER thread
// while a step is in flight is that thread hitting a software
// breakpoint (INT3), not the step completing — classify it as a
// breakpoint so the engine's step-over state machine isn't fed a
// bogus StopSingleStep for the wrong thread.
if b.stepping && tid == b.stepTID {
b.stepping = false
b.stepTID = 0
return StopEvent{Reason: StopSingleStep, TID: tid}, nil
}
return StopEvent{
Reason: StopBreakpoint,
TID: tid,
}, nil
default:
if err := b.continueIfTraceeExists(tid, 0); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT trap cause %d tid %d: %w", cause, tid, err)
}
continue
}
}
if sig == syscall.SIGSTOP && tid != b.pid {
// A newly cloned thread's initial group-stop. With
// PTRACE_O_TRACECLONE the kernel auto-attaches it and it inherits
// our ptrace options, so we just resume THIS thread. Crucially we
// must NOT touch the rest of the group: another thread may be
// stopped at a breakpoint waiting for the engine, and a
// group-continue here would let it run away (the exact "parking the
// thread group" hazard that kept clone tracing disabled before).
if err := b.continueIfTraceeExists(tid, 0); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT new thread tid %d: %w", tid, err)
}
continue
}
// SIGURG is Go's goroutine-preemption signal; it must be re-delivered
// transparently during both step and continue or scheduling breaks.
if sig == syscall.SIGURG {
// Re-issue the single-step only for the thread actually being
// stepped; a SIGURG on any other thread must be re-delivered and
// the thread continued, never single-stepped.
if b.stepping && tid == b.stepTID {
if err := b.singleStepIfTraceeExists(tid); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_SINGLESTEP after SIGURG tid %d: %w", tid, err)
}
} else {
if err := b.continueIfTraceeExists(tid, int(sig)); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT SIGURG tid %d: %w", tid, err)
}
}
continue
}
if sig == syscall.SIGCONT {
if err := b.continueIfTraceeExists(tid, 0); err != nil {
return StopEvent{}, fmt.Errorf("PTRACE_CONT SIGCONT tid %d: %w", tid, err)
}
continue
}
b.recordStop(tid)
return StopEvent{
Reason: StopSignal,
TID: tid,
Signal: int(sig),
}, nil
}
}
var _ Backend = (*linuxBackend)(nil)
func (b *linuxBackend) setPID(pid int) {
b.pid = pid
b.lastStopTID = pid
}
func (b *linuxBackend) traceTID() int {
if b.lastStopTID != 0 {
return b.lastStopTID
}
return b.pid
}
func (b *linuxBackend) recordStop(tid int) {
if tid != 0 {
b.lastStopTID = tid
}
}
func isNoSuchProcess(err error) bool {
return errors.Is(err, syscall.ESRCH) || errors.Is(err, os.ErrNotExist)
}
func isNoChildProcess(err error) bool {
return errors.Is(err, syscall.ECHILD)
}
func (b *linuxBackend) continueIfTraceeExists(tid int, signal int) error {
if tid == 0 {
return nil
}
var err error
b.execPtrace(func() { err = syscall.PtraceCont(tid, signal) })
if err != nil && !isNoSuchProcess(err) {
return err
}
return nil
}
func (b *linuxBackend) singleStepIfTraceeExists(tid int) error {
if tid == 0 {
return nil
}
var err error
b.execPtrace(func() { err = syscall.PtraceSingleStep(tid) })
if err != nil && !isNoSuchProcess(err) {
return err
}
return nil
}