-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpcontrol.go
929 lines (835 loc) · 27.9 KB
/
pcontrol.go
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
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
// Package pcontrol allows you to attach to a running process and call system calls from inside the attached process.
//
// It works on Linux and internally uses ptrace.
package pcontrol
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"runtime"
"unsafe"
"github.com/google/uuid"
"gitlab.com/tozd/go/errors"
"golang.org/x/sys/unix"
)
var (
ErrProcessAlreadyAttached = errors.Base("process already attached")
ErrProcessNotAttached = errors.Base("process not attached")
ErrOutOfMemory = errors.Base("syscall payload is larger than available memory")
ErrUnexpectedRead = errors.Base("unexpected bytes read")
ErrUnexpectedWrite = errors.Base("unexpected bytes written")
ErrUnexpectedWaitStatus = errors.Base("unexpected wait status")
)
const (
// These errno values are not really meant for user space programs (so they are not defined
// in unix package) but we need them as we operate on a lower level and handle them in doSyscall.
_ERESTARTSYS = unix.Errno(512) //nolint: revive,stylecheck
_ERESTARTNOINTR = unix.Errno(513) //nolint: revive,stylecheck
_ERESTARTNOHAND = unix.Errno(514) //nolint: revive,stylecheck
_ERESTART_RESTARTBLOCK = unix.Errno(516) //nolint: revive,stylecheck
)
// Errors are returned as negative numbers from syscalls but we compare them as uint64.
const maxErrno = uint64(0xfffffffffffff001)
const (
dataSize = 1024
controlSize = 1024
memoryAlignmentBytes = 8
)
// DefaultMemorySize is the default memory size of the allocated private working memory when attaching to the process.
const DefaultMemorySize = 4096
// We want to return -1 as uint64 so we need a variable to make Go happy.
var errorReturn = -1 //nolint:gochecknoglobals
func newMsghrd(start uint64, iov, control []byte) (uint64, []byte, errors.E) {
buf := new(bytes.Buffer)
// We build unix.Iovec.Base in the buffer.
e := binary.Write(buf, nativeEndian, iov)
if e != nil {
return 0, nil, errors.WithStack(e)
}
// We build unix.Msghdr.Control in the buffer.
e = binary.Write(buf, nativeEndian, control)
if e != nil {
return 0, nil, errors.WithStack(e)
}
// We build unix.Iovec in the buffer.
// Base field.
e = binary.Write(buf, nativeEndian, start)
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Len field.
e = binary.Write(buf, nativeEndian, uint64(len(iov)))
if e != nil {
return 0, nil, errors.WithStack(e)
}
offset := uint64(buf.Len())
// We build unix.Msghdr in the buffer.
// Name field. Null pointer.
e = binary.Write(buf, nativeEndian, uint64(0))
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Namelen field.
e = binary.Write(buf, nativeEndian, uint32(0))
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Pad_cgo_0 field.
e = binary.Write(buf, nativeEndian, [4]byte{})
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Iov field.
e = binary.Write(buf, nativeEndian, start+uint64(len(iov))+uint64(len(control)))
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Iovlen field.
e = binary.Write(buf, nativeEndian, uint64(1))
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Control field.
e = binary.Write(buf, nativeEndian, start+uint64(len(iov)))
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Controllen field.
e = binary.Write(buf, nativeEndian, uint64(len(control)))
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Flags field.
e = binary.Write(buf, nativeEndian, int32(0))
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Pad_cgo_1 field.
e = binary.Write(buf, nativeEndian, [4]byte{})
if e != nil {
return 0, nil, errors.WithStack(e)
}
// Sanity check.
if uint64(buf.Len())-offset != uint64(unsafe.Sizeof(unix.Msghdr{})) { //nolint:exhaustruct
panic(errors.New("msghdr in buffer does not match the size of msghdr"))
}
return offset, buf.Bytes(), nil
}
func alignMemory(x uint64) uint64 {
return ((x + (memoryAlignmentBytes - 1)) / memoryAlignmentBytes) * memoryAlignmentBytes
}
type Process struct {
// Pid of the process to control (and attach to).
Pid int
// MemorySize of the allocated private working memory. Default is DefaultMemorySize.
MemorySize uint64
// LogWarnf is a function to call with any warning logging messages.
LogWarnf func(msg string, args ...any)
memoryAddress uint64
}
// Attach attaches to the process and allocates private working memory in it.
//
// While the process is attached to, its regular execution is paused and only
// signal processing happens in the process.
func (p *Process) Attach() (errE errors.E) { //nolint:nonamedreturns
if p.memoryAddress != 0 {
return errors.WithDetails(ErrProcessAlreadyAttached, "pid", p.Pid)
}
runtime.LockOSThread()
err := unix.PtraceSeize(p.Pid)
if err != nil {
runtime.UnlockOSThread()
errE = errors.WithMessage(err, "ptrace seize")
errors.Details(errE)["pid"] = p.Pid
return errE
}
defer func() {
if errE != nil {
err = unix.PtraceDetach(p.Pid)
runtime.UnlockOSThread()
if err != nil {
errE2 := errors.WithMessage(err, "ptrace detach")
errors.Details(errE2)["pid"] = p.Pid
errE = errors.Join(errE, errE2)
}
}
}()
err = unix.PtraceInterrupt(p.Pid)
if err != nil {
errE = errors.WithMessage(err, "ptrace interrupt")
errors.Details(errE)["pid"] = p.Pid
return errE
}
errE = p.waitTrap(unix.PTRACE_EVENT_STOP)
if errE != nil {
errors.Details(errE)["pid"] = p.Pid
return errE
}
address, errE := p.allocateMemory()
if errE != nil {
errors.Details(errE)["pid"] = p.Pid
return errE
}
p.memoryAddress = address
return nil
}
// Detach detaches from the process and frees the allocated private working memory in it.
func (p *Process) Detach() errors.E {
if p.memoryAddress == 0 {
return errors.WithDetails(ErrProcessNotAttached, "pid", p.Pid)
}
errE1 := p.freeMemory(p.memoryAddress)
if errE1 != nil {
errors.Details(errE1)["pid"] = p.Pid
// We do not return the error here, we try to detatch the process as well.
}
err := unix.PtraceDetach(p.Pid)
runtime.UnlockOSThread()
if err != nil {
errE2 := errors.WithMessage(err, "ptrace detach")
errors.Details(errE2)["pid"] = p.Pid
// errE1 can be nil here and then this is the same as return errE2.
return errors.Join(errE1, errE2)
}
p.memoryAddress = 0
return errE1
}
// GetFds does a cross-process duplication of file descriptors from the (attached) process into this (host) process.
//
// It uses an abstract unix domain socket to get processFds from the process. If any of processFds
// are not found in the process, -1 is used in hostFds for it instead and no error is reported.
//
// You should close processFds afterwards if they are not needed anymore in the (attached) process.
// Same for hostFds in this (host) process.
func (p *Process) GetFds(processFds []int) (hostFds []int, errE errors.E) { //nolint:nonamedreturns
if p.memoryAddress == 0 {
return nil, errors.WithDetails(ErrProcessNotAttached, "pid", p.Pid)
}
// Address starting with @ signals that this is an abstract unix domain socket.
u, err := uuid.NewRandom()
if err != nil {
return nil, errors.WithMessage(err, "uuid new")
}
addr := fmt.Sprintf("@dinit-%s.sock", u.String())
processSocket, errE := p.SysSocket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if errE != nil {
return nil, errE
}
defer func() {
errE2 := p.SysClose(processSocket)
errE = errors.Join(errE, errE2)
}()
errE = p.SysBindUnix(processSocket, addr)
if errE != nil {
return nil, errE
}
errE = p.SysListen(processSocket, 1)
if errE != nil {
return nil, errE
}
connection, err := net.Dial("unix", addr)
if err != nil {
return nil, errors.WithMessage(err, "net dial")
}
defer connection.Close()
unixConnection, ok := connection.(*net.UnixConn)
if !ok {
panic(errors.Errorf("connection is %T and not net.UnixConn", connection))
}
processConnection, errE := p.SysAccept(processSocket, 0)
if errE != nil {
return nil, errE
}
defer func() {
errE2 := p.SysClose(processConnection)
errE = errors.Join(errE, errE2)
}()
for _, processFd := range processFds {
// Encode the file descriptor.
rights := unix.UnixRights(processFd)
// Send it over. Write always returns error on short writes.
// We send one byte data just to be sure everything gets through.
_, _, errE = p.SysSendmsg(processConnection, []byte{0}, rights, 0)
if errE != nil {
if errors.Is(errE, unix.EBADF) {
hostFds = append(hostFds, -1)
continue
}
return hostFds, errE
}
// We could be more precise with needed sizes here, but it is good enough.
iov := make([]byte, dataSize)
control := make([]byte, controlSize)
// TODO: What to do on short reads?
_, controln, _, _, err := unixConnection.ReadMsgUnix(iov, control)
if err != nil {
return hostFds, errors.WithMessage(err, "read msg unix")
}
// The buffer might not been used fully.
control = control[:controln]
cmsgs, err := unix.ParseSocketControlMessage(control)
if err != nil {
return hostFds, errors.WithMessage(err, "parse socket control message")
}
for _, cmsg := range cmsgs {
fds, err := unix.ParseUnixRights(&cmsg)
if err != nil {
return hostFds, errors.WithMessage(err, "parse unix rights")
}
hostFds = append(hostFds, fds...)
}
}
return hostFds, nil
}
// SetFd does a cross-process duplication of a file descriptor from this (host) process into the (attached) process.
//
// It uses an abstract unix domain socket to send hostFd to the process and then dup3 syscall
// to set that file descriptor to processFd in the process (any previous processFd is closed
// by dup3).
//
// You should close hostFd afterwards if it is not needed anymore in this (host) process.
// Same for processFd in the (attached) process.
func (p *Process) SetFd(hostFd int, processFd int) (errE errors.E) { //nolint:nonamedreturns
if p.memoryAddress == 0 {
return errors.WithDetails(ErrProcessNotAttached, "pid", p.Pid)
}
// Address starting with @ signals that this is an abstract unix domain socket.
u, err := uuid.NewRandom()
if err != nil {
return errors.WithMessage(err, "uuid new")
}
addr := fmt.Sprintf("@dinit-%s.sock", u.String())
listen, err := net.Listen("unix", addr)
if err != nil {
return errors.WithMessage(err, "net listen")
}
defer listen.Close()
// SOCK_DGRAM did not work so we use SOCK_STREAM.
// See: https://stackoverflow.com/questions/76327509/sending-a-file-descriptor-from-go-to-c
processSocket, errE := p.SysSocket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if errE != nil {
return errE
}
defer func() {
errE2 := p.SysClose(processSocket)
errE = errors.Join(errE, errE2)
}()
errE = p.SysConnectUnix(processSocket, addr)
if errE != nil {
return errE
}
connection, err := listen.Accept()
if err != nil {
return errors.WithMessage(err, "accept")
}
defer connection.Close()
unixConnection, ok := connection.(*net.UnixConn)
if !ok {
panic(errors.Errorf("connection is %T and not net.UnixConn", connection))
}
// Encode the file descriptor.
rights := unix.UnixRights(hostFd)
// Send it over. Write always returns error on short writes.
// We send one byte data just to be sure everything gets through.
_, _, err = unixConnection.WriteMsgUnix([]byte{0}, rights, nil)
if err != nil {
return errors.WithMessage(err, "write msg unix")
}
// We could be more precise with needed sizes here, but it is good enough.
iov := make([]byte, dataSize)
control := make([]byte, controlSize)
// TODO: What to do on short reads?
_, controln, _, errE := p.SysRecvmsg(processSocket, iov, control, 0)
if errE != nil {
return errE
}
// The buffer might not been used fully.
control = control[:controln]
cmsgs, err := unix.ParseSocketControlMessage(control)
if err != nil {
return errors.WithMessage(err, "parse socket control message")
}
fds, err := unix.ParseUnixRights(&cmsgs[0])
if err != nil {
return errors.WithMessage(err, "parse unix rights")
}
fd := fds[0]
errE = p.SysDup3(fd, processFd)
if errE != nil {
return errE
}
errE = p.SysClose(fd)
if errE != nil {
return errE
}
return nil
}
func (p *Process) memorySize() uint64 {
if p.MemorySize == 0 {
return DefaultMemorySize
}
return p.MemorySize
}
// Allocate private segment of memory in the process. We use it as
// the working memory for syscalls. Memory is configured to be
// executable as well and we store opcodes to run into it as well.
func (p *Process) allocateMemory() (uint64, errors.E) {
addr, err := p.doSyscall(false, unix.SYS_MMAP, func(_ uint64) ([]byte, [6]uint64, errors.E) {
fd := -1
return nil, [6]uint64{
0, // addr.
p.memorySize(), // length.
unix.PROT_EXEC | unix.PROT_READ | unix.PROT_WRITE, // prot.
unix.MAP_PRIVATE | unix.MAP_ANONYMOUS, // flags.
uint64(fd), // fd.
0, // offset.
}, nil
})
if addr == 0 {
err = errors.New("invalid result")
}
return addr, errors.WithMessage(err, "allocate memory")
}
// Free private segment of memory in the process.
func (p *Process) freeMemory(address uint64) errors.E {
_, err := p.doSyscall(false, unix.SYS_MUNMAP, func(_ uint64) ([]byte, [6]uint64, errors.E) {
return nil, [6]uint64{
address, // addr.
p.memorySize(), // length.
}, nil
})
return errors.WithMessage(err, "free memory")
}
// Getpid invokes getpid syscall in the (attached) process.
func (p *Process) SysGetpid() (int, errors.E) {
pid, err := p.doSyscall(true, unix.SYS_GETPID, func(_ uint64) ([]byte, [6]uint64, errors.E) {
return nil, [6]uint64{}, nil
})
return int(pid), errors.WithMessage(err, "sys getpid") //nolint:gosec
}
// SysSocket invokes socket syscall in the (attached) process.
func (p *Process) SysSocket(domain, typ, proto int) (int, errors.E) {
fd, err := p.doSyscall(true, unix.SYS_SOCKET, func(_ uint64) ([]byte, [6]uint64, errors.E) {
return nil, [6]uint64{
uint64(domain), // domain.
uint64(typ), // type.
uint64(proto), // protocol.
}, nil
})
return int(fd), errors.WithMessage(err, "sys socket") //nolint:gosec
}
// SysClose invokes close syscall in the (attached) process.
func (p *Process) SysClose(fd int) errors.E {
_, err := p.doSyscall(true, unix.SYS_CLOSE, func(_ uint64) ([]byte, [6]uint64, errors.E) {
return nil, [6]uint64{
uint64(fd), // fd.
}, nil
})
return errors.WithMessage(err, "sys close")
}
// SysListen invokes listen syscall in the (attached) process.
func (p *Process) SysListen(fd, backlog int) errors.E {
_, err := p.doSyscall(true, unix.SYS_LISTEN, func(_ uint64) ([]byte, [6]uint64, errors.E) {
return nil, [6]uint64{
uint64(fd), // sockfd.
uint64(backlog), // backlog.
}, nil
})
return errors.WithMessage(err, "sys listen")
}
// SysAccept invokes accept syscall in the (attached) process.
func (p *Process) SysAccept(fd, flags int) (int, errors.E) {
connFd, err := p.doSyscall(true, unix.SYS_ACCEPT4, func(_ uint64) ([]byte, [6]uint64, errors.E) {
return nil, [6]uint64{
uint64(fd), // sockfd.
0, // addr.
0, // addrlen.
uint64(flags), // flags.
}, nil
})
return int(connFd), errors.WithMessage(err, "sys accept") //nolint:gosec
}
// SysDup3 invokes dup3 syscall in the (attached) process.
func (p *Process) SysDup3(oldFd, newFd int) errors.E {
_, err := p.doSyscall(true, unix.SYS_DUP3, func(_ uint64) ([]byte, [6]uint64, errors.E) {
return nil, [6]uint64{
uint64(oldFd), // oldfd.
uint64(newFd), // newfd.
0, // flags.
}, nil
})
return errors.WithMessage(err, "sys dup3")
}
// SysConnectUnix invokes connect syscall in the (attached) process for AF_UNIX socket path.
//
// If path starts with @, it is replaced with null character to connect to an abstract unix domain socket.
func (p *Process) SysConnectUnix(fd int, path string) errors.E {
return p.connectOrBindUnix(unix.SYS_CONNECT, "connect", fd, path)
}
// SysBindUnix invokes bind syscall in the (attached) process for AF_UNIX socket path.
//
// If path starts with @, it is replaced with null character to bind to an abstract unix domain socket.
func (p *Process) SysBindUnix(fd int, path string) errors.E {
return p.connectOrBindUnix(unix.SYS_BIND, "bind", fd, path)
}
// Both connect and bind system calls take the same arguments, so we have one method for both.
func (p *Process) connectOrBindUnix(call int, name string, fd int, path string) errors.E {
_, err := p.doSyscall(true, call, func(start uint64) ([]byte, [6]uint64, errors.E) {
buf := new(bytes.Buffer)
// We build unix.RawSockaddrUnix in the buffer.
// Family field.
err := binary.Write(buf, nativeEndian, uint16(unix.AF_UNIX))
if err != nil {
return nil, [6]uint64{}, errors.WithStack(err)
}
p := []byte(path)
abstract := false
// If it starts with @, it is an abstract unix domain socket.
// We change @ to a null character.
if p[0] == '@' {
p[0] = 0
abstract = true
} else if p[0] == 0 {
abstract = true
}
// Path field.
err = binary.Write(buf, nativeEndian, p)
if err != nil {
return nil, [6]uint64{}, errors.WithStack(err)
}
if !abstract {
// If not abstract, then write a null character.
err = binary.Write(buf, nativeEndian, uint8(0))
if err != nil {
return nil, [6]uint64{}, errors.WithStack(err)
}
}
// Sanity check.
if uint64(buf.Len()) > uint64(unsafe.Sizeof(unix.RawSockaddrUnix{})) { //nolint:exhaustruct
panic(errors.New("path too long"))
}
payload := buf.Bytes()
return payload, [6]uint64{
uint64(fd), // sockfd.
start, // addr.
uint64(len(payload)), // addrlen.
}, nil
})
return errors.WithMessagef(err, "sys %s unix", name)
}
// SysSendmsg invokes sendmsg syscall in the (attached) process.
func (p *Process) SysSendmsg(fd int, iov, control []byte, flags int) (int, int, errors.E) {
var payload []byte
res, err := p.doSyscall(true, unix.SYS_SENDMSG, func(start uint64) ([]byte, [6]uint64, errors.E) {
offset, pl, err := newMsghrd(start, iov, control)
if err != nil {
return nil, [6]uint64{}, err
}
payload = pl
return payload, [6]uint64{
uint64(fd), // sockfd.
start + offset, // msg.
uint64(flags), // flags.
}, nil
})
if err != nil {
return int(res), 0, errors.WithMessage(err, "sys sendmsg") //nolint:gosec
}
return int(res), len(control), nil //nolint:gosec
}
// SysRecvmsg invokes recvmsg syscall in the (attached) process.
//
//nolint:mnd
func (p *Process) SysRecvmsg(fd int, iov, control []byte, flags int) (int, int, int, errors.E) {
var payload []byte
res, errE := p.doSyscall(true, unix.SYS_RECVMSG, func(start uint64) ([]byte, [6]uint64, errors.E) {
offset, pl, err := newMsghrd(start, iov, control)
if err != nil {
return nil, [6]uint64{}, err
}
payload = pl
return payload, [6]uint64{
uint64(fd), // sockfd.
start + offset, // msg.
uint64(flags), // flags.
}, nil
})
if errE != nil {
return int(res), 0, 0, errors.WithMessage(errE, "sys recvmsg") //nolint:gosec
}
buf := bytes.NewReader(payload)
err := binary.Read(buf, nativeEndian, iov) // unix.Iovec.Base.
if err != nil {
return int(res), 0, 0, errors.WithMessage(err, "sys recvmsg") //nolint:gosec
}
err = binary.Read(buf, nativeEndian, control) // unix.Msghdr.Control.
if err != nil {
return int(res), 0, 0, errors.WithMessage(err, "sys recvmsg") //nolint:gosec
}
_, _ = io.CopyN(io.Discard, buf, 8) // unix.Iovec.Base field.
_, _ = io.CopyN(io.Discard, buf, 8) // unix.Iovec.Len field.
_, _ = io.CopyN(io.Discard, buf, 8) // Name field.
_, _ = io.CopyN(io.Discard, buf, 4) // Namelen field.
_, _ = io.CopyN(io.Discard, buf, 4) // Pad_cgo_0 field.
_, _ = io.CopyN(io.Discard, buf, 8) // Iov field.
_, _ = io.CopyN(io.Discard, buf, 8) // Iovlen field.
_, _ = io.CopyN(io.Discard, buf, 8) // Control field.
var controln uint64
err = binary.Read(buf, nativeEndian, &controln) // Controllen field.
if err != nil {
return int(res), 0, 0, errors.WithMessage(err, "sys recvmsg") //nolint:gosec
}
var recvflags int32
err = binary.Read(buf, nativeEndian, &recvflags) // Flags field.
if err != nil {
return int(res), 0, 0, errors.WithMessage(err, "sys recvmsg") //nolint:gosec
}
return int(res), int(controln), int(recvflags), nil //nolint:gosec
}
// Low-level call of a system call in the process. Use doSyscall instead.
// In almost all cases you want to use it with useMemory set to true to
// not change code of the process to run a syscall. (We use useMemory set
// to false only to obtain and free such memory.)
func (p *Process) syscall(useMemory bool, call int, args func(start uint64) ([]byte, [6]uint64, errors.E)) (result uint64, err errors.E) { //nolint:nonamedreturns
if useMemory && p.memoryAddress == 0 {
return uint64(errorReturn), errors.WithDetails(ErrProcessNotAttached, "pid", p.Pid)
}
var originalRegs processRegs
originalRegs, err = getProcessRegs(p.Pid)
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
var start uint64
var payload []byte
var payloadLength uint64
var arguments [6]uint64
var originalInstructions []byte
if useMemory {
start = p.memoryAddress
payload, arguments, err = args(p.memoryAddress)
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
payloadLength = alignMemory(uint64(len(payload)))
availableMemory := p.memorySize() - uint64(len(syscallInstruction))
if payloadLength > availableMemory {
return uint64(errorReturn), errors.WithDetails(
ErrOutOfMemory,
"call", call,
"payload", payloadLength,
"available", availableMemory,
)
}
} else {
start = alignMemory(getProcessPC(&originalRegs))
payload, arguments, err = args(start)
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
payloadLength = alignMemory(uint64(len(payload)))
// TODO: What if payload is so large that it hits the end of the data section?
originalInstructions, err = p.readData(uintptr(start), int(payloadLength)+len(syscallInstruction)) //nolint:gosec
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
}
defer func() {
err2 := setProcessRegs(p.Pid, &originalRegs)
if err2 != nil {
errors.Details(err2)["call"] = call
}
err = errors.Join(err, err2)
}()
if !useMemory {
defer func() {
err2 := p.writeData(uintptr(start), originalInstructions)
if err2 != nil {
errors.Details(err2)["call"] = call
}
err = errors.Join(err, err2)
}()
}
err = p.writeData(uintptr(start), payload)
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
instructionPointer := start + payloadLength
err = p.writeData(uintptr(instructionPointer), syscallInstruction[:])
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
newRegs := newSyscallRegs(&originalRegs, instructionPointer, call, arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5])
err = setProcessRegs(p.Pid, &newRegs)
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
err = p.runToBreakpoint()
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
var resultRegs processRegs
resultRegs, err = getProcessRegs(p.Pid)
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
resReg := getSyscallResultReg(&resultRegs)
if resReg > maxErrno {
return uint64(errorReturn), errors.WithDetails(
unix.Errno(-resReg),
"call", call,
)
}
newPayload, err := p.readData(uintptr(start), len(payload))
if err != nil {
errors.Details(err)["call"] = call
return uint64(errorReturn), err
}
copy(payload, newPayload)
return resReg, nil
}
// Syscalls can be interrupted by signal handling and might abort. So we
// wrap them with a loop which retries them automatically if interrupted.
// We do not handle EAGAIN here on purpose, to not block in a loop.
func (p *Process) doSyscall(useMemory bool, call int, args func(start uint64) ([]byte, [6]uint64, errors.E)) (uint64, errors.E) {
for {
result, err := p.syscall(useMemory, call, args)
if err != nil {
if errors.Is(err, _ERESTARTSYS) {
continue
} else if errors.Is(err, _ERESTARTNOINTR) {
continue
} else if errors.Is(err, _ERESTARTNOHAND) {
continue
} else if errors.Is(err, _ERESTART_RESTARTBLOCK) {
continue
} else if errors.Is(err, unix.EINTR) {
continue
}
// Go to return.
}
return result, err
}
}
// Syscall invokes a syscall with given arguments and returns its return value.
//
// Arguments are returned from the args callback and can be provided through a byte slice or through
// 6 uint64 arguments. The byte slice is copied to the (attached) process memory (into allocated
// private working memory) at start and you can then reference that memory through 6 uint64 arguments.
//
// Return value is -1 on error and a corresponding errno value is returned as error.
func (p *Process) Syscall(call int, args func(start uint64) ([]byte, [6]uint64, errors.E)) (uint64, errors.E) {
return p.doSyscall(true, call, args)
}
// Read from the memory of the process.
func (p *Process) readData(address uintptr, length int) ([]byte, errors.E) {
data := make([]byte, length)
n, err := unix.PtracePeekData(p.Pid, address, data)
if err != nil {
return nil, errors.WithMessage(err, "ptrace peekdata")
}
if n != length {
return nil, errors.WithDetails(
ErrUnexpectedRead,
"expected", length,
"read", n,
)
}
return data, nil
}
// Read into the memory of the process.
func (p *Process) writeData(address uintptr, data []byte) errors.E {
n, err := unix.PtracePokeData(p.Pid, address, data)
if err != nil {
return errors.WithMessage(err, "ptrace pokedata")
}
if n != len(data) {
return errors.WithDetails(
ErrUnexpectedRead,
"expected", len(data),
"written", n,
)
}
return nil
}
// When we do a syscall we set opcodes to call a syscall and we put afterwards
// a breakpoint (see syscallInstruction). This function executes those opcodes
// and returns once we hit the breakpoint. During execution signal handlers
// of the trustee might run as well before the breakpoint is reached (this is
// why we use ptrace cont with a breakpoint and not ptrace single step).
func (p *Process) runToBreakpoint() errors.E {
err := unix.PtraceCont(p.Pid, 0)
if err != nil {
return errors.WithMessage(err, "ptrace cont")
}
// 0 trap cause means a breakpoint or single stepping.
return p.waitTrap(0)
}
func (p *Process) waitTrap(cause int) errors.E {
for {
var status unix.WaitStatus
var err error
for {
_, err = unix.Wait4(p.Pid, &status, 0, nil)
if err == nil || !errors.Is(err, unix.EINTR) {
break
}
}
if err != nil {
return errors.WithMessage(err, "wait4")
}
// A breakpoint or other trap cause we expected has been reached.
if status.TrapCause() == cause {
return nil
} else if status.TrapCause() != -1 {
if p.LogWarnf != nil {
p.LogWarnf("unexpected trap cause for PID %d: %d, expected %d", p.Pid, status.TrapCause(), cause)
}
return nil
} else if status.Stopped() {
// If the process stopped it might have stopped for some other signal. While a process is
// ptraced any signal it receives stops the process for us to decide what to do about the
// signal. In our case we just pass the signal back to the process using ptrace cont and
// let its signal handler do its work.
err := unix.PtraceCont(p.Pid, int(status.StopSignal()))
if err != nil {
errE := errors.WithMessage(err, "ptrace cont")
errors.Details(errE)["stopSignal"] = int(status.StopSignal())
return errE
}
continue
}
return errors.WithDetails(
ErrUnexpectedWaitStatus,
"exitStatus", status.ExitStatus(),
"signal", status.Signal(),
"stopSignal", status.StopSignal(),
"trapCause", status.TrapCause(),
"expectedTrapCause", cause,
)
}
}
// EqualFds returns true if both file descriptors point to the same underlying file.
func EqualFds(fd1, fd2 int) (bool, errors.E) {
var stat1 unix.Stat_t
err := unix.Fstat(fd1, &stat1)
if err != nil {
return false, errors.WithMessage(err, "fstat")
}
var stat2 unix.Stat_t
err = unix.Fstat(fd2, &stat2)
if err != nil {
return false, errors.WithMessage(err, "fstat")
}
return stat1.Dev == stat2.Dev && stat1.Ino == stat2.Ino && stat1.Rdev == stat2.Rdev, nil
}