rptrace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +72 -0
- data/examples/file_access_tracer.rb +17 -0
- data/examples/memory_reader.rb +71 -0
- data/examples/simple_strace.rb +16 -0
- data/examples/syscall_counter.rb +21 -0
- data/lib/rptrace/binding.rb +88 -0
- data/lib/rptrace/breakpoint.rb +46 -0
- data/lib/rptrace/c_structs.rb +147 -0
- data/lib/rptrace/constants.rb +128 -0
- data/lib/rptrace/dsl.rb +162 -0
- data/lib/rptrace/error.rb +30 -0
- data/lib/rptrace/event.rb +134 -0
- data/lib/rptrace/memory.rb +153 -0
- data/lib/rptrace/permission.rb +97 -0
- data/lib/rptrace/proc_maps.rb +107 -0
- data/lib/rptrace/registers.rb +143 -0
- data/lib/rptrace/seccomp_event.rb +25 -0
- data/lib/rptrace/syscall.rb +124 -0
- data/lib/rptrace/syscall_event.rb +355 -0
- data/lib/rptrace/syscall_table/aarch64.rb +24 -0
- data/lib/rptrace/syscall_table/generator.rb +172 -0
- data/lib/rptrace/syscall_table/x86_64.rb +60 -0
- data/lib/rptrace/tracee.rb +469 -0
- data/lib/rptrace/version.rb +6 -0
- data/lib/rptrace.rb +49 -0
- metadata +86 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Rptrace
|
|
6
|
+
# Architecture-specific register layouts and binary helpers.
|
|
7
|
+
module CStructs
|
|
8
|
+
# Host pointer width in bytes.
|
|
9
|
+
POINTER_SIZE = begin
|
|
10
|
+
[0].pack("J").bytesize
|
|
11
|
+
rescue StandardError
|
|
12
|
+
8
|
|
13
|
+
end
|
|
14
|
+
# Pointer pack format for host pointer size.
|
|
15
|
+
POINTER_PACK = POINTER_SIZE == 8 ? "Q<" : "L<"
|
|
16
|
+
# Register word width in bytes.
|
|
17
|
+
WORD_SIZE = 8
|
|
18
|
+
# Register word bitmask.
|
|
19
|
+
WORD_MASK = (1 << (WORD_SIZE * 8)) - 1
|
|
20
|
+
# Register word unpack/pack format.
|
|
21
|
+
PACK_FORMAT = "Q<"
|
|
22
|
+
# seccomp metadata struct pack format.
|
|
23
|
+
SECCOMP_METADATA_FORMAT = "Q<Q<"
|
|
24
|
+
# seccomp metadata struct size.
|
|
25
|
+
SECCOMP_METADATA_SIZE = 16
|
|
26
|
+
# BPF instruction size in seccomp filter dump.
|
|
27
|
+
SECCOMP_FILTER_INSN_SIZE = 8
|
|
28
|
+
|
|
29
|
+
# x86_64 user_regs_struct register names.
|
|
30
|
+
X86_64_REGS = %i[
|
|
31
|
+
r15 r14 r13 r12 rbp rbx r11 r10 r9 r8
|
|
32
|
+
rax rcx rdx rsi rdi orig_rax rip cs eflags
|
|
33
|
+
rsp ss fs_base gs_base ds es fs gs
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
# aarch64 register names returned by NT_PRSTATUS regset.
|
|
37
|
+
AARCH64_REGS = (0..30).map { |i| :"x#{i}" }.push(:sp, :pc, :pstate).freeze
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# @return [Symbol] :x86_64 or :aarch64
|
|
42
|
+
def arch
|
|
43
|
+
@arch ||= detect_arch
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param arch [Symbol]
|
|
47
|
+
# @return [Array<Symbol>]
|
|
48
|
+
def reg_names(arch: self.arch)
|
|
49
|
+
case arch
|
|
50
|
+
when :x86_64 then X86_64_REGS
|
|
51
|
+
when :aarch64 then AARCH64_REGS
|
|
52
|
+
else
|
|
53
|
+
raise UnsupportedArchError, "Unsupported architecture: #{arch}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param arch [Symbol]
|
|
58
|
+
# @return [Integer]
|
|
59
|
+
def regs_size(arch: self.arch)
|
|
60
|
+
reg_names(arch: arch).size * 8
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param source [String, Fiddle::Pointer]
|
|
64
|
+
# @param arch [Symbol]
|
|
65
|
+
# @return [Hash<Symbol, Integer>]
|
|
66
|
+
def decode_regs(source, arch: self.arch)
|
|
67
|
+
bytes = source.is_a?(String) ? source : source[0, regs_size(arch: arch)]
|
|
68
|
+
values = bytes.unpack("#{PACK_FORMAT}*")
|
|
69
|
+
names = reg_names(arch: arch)
|
|
70
|
+
|
|
71
|
+
names.each_with_index.each_with_object({}) do |(name, index), decoded|
|
|
72
|
+
decoded[name] = values.fetch(index, 0)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param regs [Hash<Symbol, Integer>]
|
|
77
|
+
# @param arch [Symbol]
|
|
78
|
+
# @return [String] packed binary register bytes
|
|
79
|
+
def encode_regs(regs, arch: self.arch)
|
|
80
|
+
names = reg_names(arch: arch)
|
|
81
|
+
values = names.map { |name| Integer(regs.fetch(name, 0)) & WORD_MASK }
|
|
82
|
+
values.pack("#{PACK_FORMAT}*")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param base [Integer]
|
|
86
|
+
# @param length [Integer]
|
|
87
|
+
# @return [String] packed iovec
|
|
88
|
+
def pack_iovec(base:, length:)
|
|
89
|
+
[Integer(base), Integer(length)].pack("#{POINTER_PACK}#{POINTER_PACK}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param bytes [String]
|
|
93
|
+
# @return [Hash<Symbol, Integer>]
|
|
94
|
+
def unpack_iovec(bytes)
|
|
95
|
+
base, length = bytes.unpack("#{POINTER_PACK}#{POINTER_PACK}")
|
|
96
|
+
{ base: base, length: length }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Integer]
|
|
100
|
+
def seccomp_metadata_size
|
|
101
|
+
SECCOMP_METADATA_SIZE
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param filter_off [Integer]
|
|
105
|
+
# @param flags [Integer]
|
|
106
|
+
# @return [String]
|
|
107
|
+
def pack_seccomp_metadata(filter_off:, flags: 0)
|
|
108
|
+
[Integer(filter_off), Integer(flags)].pack(SECCOMP_METADATA_FORMAT)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param bytes [String]
|
|
112
|
+
# @return [Hash<Symbol, Integer>]
|
|
113
|
+
def unpack_seccomp_metadata(bytes)
|
|
114
|
+
filter_off, flags = bytes.unpack(SECCOMP_METADATA_FORMAT)
|
|
115
|
+
{ filter_off: filter_off, flags: flags }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param bytes [String]
|
|
119
|
+
# @return [Array<Hash<Symbol, Integer>>]
|
|
120
|
+
def decode_seccomp_filter(bytes)
|
|
121
|
+
blob = bytes.to_s.b
|
|
122
|
+
raise ArgumentError, "seccomp filter bytes must align to #{SECCOMP_FILTER_INSN_SIZE}" unless (blob.bytesize % SECCOMP_FILTER_INSN_SIZE).zero?
|
|
123
|
+
|
|
124
|
+
instructions = []
|
|
125
|
+
blob.bytes.each_slice(SECCOMP_FILTER_INSN_SIZE) do |insn|
|
|
126
|
+
code, jt, jf, k = insn.pack("C*").unpack("S<CCL<")
|
|
127
|
+
instructions << { code: code, jt: jt, jf: jf, k: k }
|
|
128
|
+
end
|
|
129
|
+
instructions
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [Symbol]
|
|
133
|
+
# @raise [Rptrace::UnsupportedArchError]
|
|
134
|
+
def detect_arch
|
|
135
|
+
host_cpu = RbConfig::CONFIG.fetch("host_cpu", "")
|
|
136
|
+
|
|
137
|
+
case host_cpu
|
|
138
|
+
when /x86_64|amd64/
|
|
139
|
+
:x86_64
|
|
140
|
+
when /aarch64|arm64/
|
|
141
|
+
:aarch64
|
|
142
|
+
else
|
|
143
|
+
raise UnsupportedArchError, "Unsupported architecture: #{host_cpu}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
# Linux ptrace and waitpid-related constants.
|
|
5
|
+
module Constants
|
|
6
|
+
NT_PRSTATUS = 1
|
|
7
|
+
|
|
8
|
+
PTRACE_TRACEME = 0
|
|
9
|
+
PTRACE_PEEKTEXT = 1
|
|
10
|
+
PTRACE_PEEKDATA = 2
|
|
11
|
+
PTRACE_PEEKUSER = 3
|
|
12
|
+
PTRACE_POKETEXT = 4
|
|
13
|
+
PTRACE_POKEDATA = 5
|
|
14
|
+
PTRACE_POKEUSER = 6
|
|
15
|
+
PTRACE_CONT = 7
|
|
16
|
+
PTRACE_KILL = 8
|
|
17
|
+
PTRACE_SINGLESTEP = 9
|
|
18
|
+
PTRACE_GETREGS = 12
|
|
19
|
+
PTRACE_SETREGS = 13
|
|
20
|
+
PTRACE_GETFPREGS = 14
|
|
21
|
+
PTRACE_SETFPREGS = 15
|
|
22
|
+
PTRACE_ATTACH = 16
|
|
23
|
+
PTRACE_DETACH = 17
|
|
24
|
+
PTRACE_GETFPXREGS = 18
|
|
25
|
+
PTRACE_SETFPXREGS = 19
|
|
26
|
+
PTRACE_SYSCALL = 24
|
|
27
|
+
PTRACE_SETOPTIONS = 0x4200
|
|
28
|
+
PTRACE_GETEVENTMSG = 0x4201
|
|
29
|
+
PTRACE_GETSIGINFO = 0x4202
|
|
30
|
+
PTRACE_SETSIGINFO = 0x4203
|
|
31
|
+
PTRACE_GETREGSET = 0x4204
|
|
32
|
+
PTRACE_SETREGSET = 0x4205
|
|
33
|
+
PTRACE_SEIZE = 0x4206
|
|
34
|
+
PTRACE_INTERRUPT = 0x4207
|
|
35
|
+
PTRACE_LISTEN = 0x4208
|
|
36
|
+
PTRACE_PEEKSIGINFO = 0x4209
|
|
37
|
+
PTRACE_GETSIGMASK = 0x420A
|
|
38
|
+
PTRACE_SETSIGMASK = 0x420B
|
|
39
|
+
PTRACE_SECCOMP_GET_FILTER = 0x420C
|
|
40
|
+
PTRACE_SECCOMP_GET_METADATA = 0x420D
|
|
41
|
+
PTRACE_GET_SYSCALL_INFO = 0x420E
|
|
42
|
+
|
|
43
|
+
PTRACE_O_TRACESYSGOOD = 0x00000001
|
|
44
|
+
PTRACE_O_TRACEFORK = 0x00000002
|
|
45
|
+
PTRACE_O_TRACEVFORK = 0x00000004
|
|
46
|
+
PTRACE_O_TRACECLONE = 0x00000008
|
|
47
|
+
PTRACE_O_TRACEEXEC = 0x00000010
|
|
48
|
+
PTRACE_O_TRACEVFORKDONE = 0x00000020
|
|
49
|
+
PTRACE_O_TRACEEXIT = 0x00000040
|
|
50
|
+
PTRACE_O_TRACESECCOMP = 0x00000080
|
|
51
|
+
PTRACE_O_EXITKILL = 0x00100000
|
|
52
|
+
PTRACE_O_SUSPEND_SECCOMP = 0x00200000
|
|
53
|
+
|
|
54
|
+
PTRACE_EVENT_FORK = 1
|
|
55
|
+
PTRACE_EVENT_VFORK = 2
|
|
56
|
+
PTRACE_EVENT_CLONE = 3
|
|
57
|
+
PTRACE_EVENT_EXEC = 4
|
|
58
|
+
PTRACE_EVENT_VFORK_DONE = 5
|
|
59
|
+
PTRACE_EVENT_EXIT = 6
|
|
60
|
+
PTRACE_EVENT_SECCOMP = 7
|
|
61
|
+
PTRACE_EVENT_STOP = 128
|
|
62
|
+
|
|
63
|
+
SECCOMP_FILTER_FLAG_TSYNC = 1
|
|
64
|
+
SECCOMP_FILTER_FLAG_LOG = 2
|
|
65
|
+
SECCOMP_FILTER_FLAG_SPEC_ALLOW = 4
|
|
66
|
+
SECCOMP_FILTER_FLAG_NEW_LISTENER = 8
|
|
67
|
+
SECCOMP_FILTER_FLAG_TSYNC_ESRCH = 16
|
|
68
|
+
|
|
69
|
+
WNOHANG = 1
|
|
70
|
+
WUNTRACED = 2
|
|
71
|
+
WCONTINUED = 8
|
|
72
|
+
WALL = 0x40000000
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Helpers mirroring wait status macros from sys/wait.h.
|
|
76
|
+
module WaitStatus
|
|
77
|
+
module_function
|
|
78
|
+
|
|
79
|
+
# @param status [Integer]
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def exited?(status)
|
|
82
|
+
(status & 0x7F).zero?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param status [Integer]
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
def signaled?(status)
|
|
88
|
+
signal = status & 0x7F
|
|
89
|
+
!signal.zero? && signal != 0x7F
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param status [Integer]
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def stopped?(status)
|
|
95
|
+
(status & 0xFF) == 0x7F
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @param status [Integer]
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
def continued?(status)
|
|
101
|
+
status == 0xFFFF
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param status [Integer]
|
|
105
|
+
# @return [Integer]
|
|
106
|
+
def exit_status(status)
|
|
107
|
+
(status >> 8) & 0xFF
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param status [Integer]
|
|
111
|
+
# @return [Integer]
|
|
112
|
+
def term_signal(status)
|
|
113
|
+
status & 0x7F
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @param status [Integer]
|
|
117
|
+
# @return [Integer]
|
|
118
|
+
def stop_signal(status)
|
|
119
|
+
(status >> 8) & 0xFF
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @param status [Integer]
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
def core_dumped?(status)
|
|
125
|
+
signaled?(status) && (status & 0x80).positive?
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/rptrace/dsl.rb
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
class << self
|
|
5
|
+
# Default option set to follow fork/clone descendants.
|
|
6
|
+
FOLLOW_CHILD_TRACE_OPTIONS = Tracee::DEFAULT_TRACE_OPTIONS |
|
|
7
|
+
Constants::PTRACE_O_TRACECLONE |
|
|
8
|
+
Constants::PTRACE_O_TRACEFORK |
|
|
9
|
+
Constants::PTRACE_O_TRACEVFORK
|
|
10
|
+
|
|
11
|
+
# Spawns and traces a command for the duration of the block.
|
|
12
|
+
#
|
|
13
|
+
# @param command [String] executable path or command name
|
|
14
|
+
# @param args [Array<String>] command arguments
|
|
15
|
+
# @param options [Integer] ptrace options passed to Tracee.spawn
|
|
16
|
+
# @yieldparam tracee [Rptrace::Tracee]
|
|
17
|
+
# @return [Object] block return value
|
|
18
|
+
def trace(command, *args, options: Tracee::DEFAULT_TRACE_OPTIONS)
|
|
19
|
+
tracee = Tracee.spawn(command, *args, options: options)
|
|
20
|
+
yield tracee
|
|
21
|
+
ensure
|
|
22
|
+
safe_detach(tracee)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Runs a command and yields syscall enter/exit events.
|
|
26
|
+
#
|
|
27
|
+
# @param command [String] executable path or command name
|
|
28
|
+
# @param args [Array<String>] command arguments
|
|
29
|
+
# @param follow_children [Boolean] follow clone/fork/vfork descendants
|
|
30
|
+
# @param yield_seccomp [Boolean] yield seccomp stop events as {Rptrace::SeccompEvent}
|
|
31
|
+
# @yieldparam event [Rptrace::SyscallEvent, Rptrace::SeccompEvent]
|
|
32
|
+
# @return [void]
|
|
33
|
+
def strace(command, *args, follow_children: false, yield_seccomp: false)
|
|
34
|
+
options = follow_children ? FOLLOW_CHILD_TRACE_OPTIONS : Tracee::DEFAULT_TRACE_OPTIONS
|
|
35
|
+
trace(command, *args, options: options) do |tracee|
|
|
36
|
+
if follow_children
|
|
37
|
+
strace_with_children(tracee, yield_seccomp: yield_seccomp) { |event| yield event }
|
|
38
|
+
else
|
|
39
|
+
strace_single(tracee, yield_seccomp: yield_seccomp) { |event| yield event }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def strace_single(tracee, yield_seccomp:)
|
|
47
|
+
loop do
|
|
48
|
+
tracee.syscall
|
|
49
|
+
event = tracee.wait(flags: Constants::WALL)
|
|
50
|
+
break if event.exited? || event.signaled?
|
|
51
|
+
if yield_seccomp && event.seccomp_event?
|
|
52
|
+
yield build_seccomp_event(tracee)
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
next unless event.syscall_stop?
|
|
56
|
+
|
|
57
|
+
syscall = tracee.current_syscall
|
|
58
|
+
syscall_args = tracee.syscall_args
|
|
59
|
+
|
|
60
|
+
yield SyscallEvent.new(tracee: tracee, syscall: syscall, args: syscall_args, phase: :enter)
|
|
61
|
+
|
|
62
|
+
tracee.syscall
|
|
63
|
+
exit_event = tracee.wait(flags: Constants::WALL)
|
|
64
|
+
break if exit_event.exited? || exit_event.signaled?
|
|
65
|
+
|
|
66
|
+
yield SyscallEvent.new(
|
|
67
|
+
tracee: tracee,
|
|
68
|
+
syscall: syscall,
|
|
69
|
+
args: syscall_args,
|
|
70
|
+
return_value: tracee.syscall_return,
|
|
71
|
+
phase: :exit
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def strace_with_children(root_tracee, yield_seccomp:)
|
|
77
|
+
tracees = { root_tracee.pid => root_tracee }
|
|
78
|
+
pending_syscalls = {}
|
|
79
|
+
begin
|
|
80
|
+
root_tracee.syscall
|
|
81
|
+
rescue NoProcessError
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
while tracees.any?
|
|
86
|
+
event = Tracee.wait_any(flags: Constants::WALL)
|
|
87
|
+
tracee = tracees.fetch(event.pid) { tracees[event.pid] = Tracee.new(event.pid) }
|
|
88
|
+
|
|
89
|
+
if event.exited? || event.signaled?
|
|
90
|
+
pending_syscalls.delete(event.pid)
|
|
91
|
+
tracees.delete(event.pid)
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if event.fork_like_event?
|
|
96
|
+
child_pid = tracee.event_message
|
|
97
|
+
child_tracee = tracees.fetch(child_pid) do
|
|
98
|
+
created = Tracee.new(child_pid)
|
|
99
|
+
created.set_options(FOLLOW_CHILD_TRACE_OPTIONS)
|
|
100
|
+
tracees[child_pid] = created
|
|
101
|
+
end
|
|
102
|
+
pending_syscalls.delete(child_pid)
|
|
103
|
+
resume_tracee_syscall(child_tracee, tracees: tracees, pending_syscalls: pending_syscalls)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if yield_seccomp && event.seccomp_event?
|
|
107
|
+
yield build_seccomp_event(tracee)
|
|
108
|
+
elsif event.syscall_stop?
|
|
109
|
+
if pending_syscalls.key?(event.pid)
|
|
110
|
+
entry = pending_syscalls.delete(event.pid)
|
|
111
|
+
yield SyscallEvent.new(
|
|
112
|
+
tracee: tracee,
|
|
113
|
+
syscall: entry.fetch(:syscall),
|
|
114
|
+
args: entry.fetch(:args),
|
|
115
|
+
return_value: tracee.syscall_return,
|
|
116
|
+
phase: :exit
|
|
117
|
+
)
|
|
118
|
+
else
|
|
119
|
+
syscall = tracee.current_syscall
|
|
120
|
+
syscall_args = tracee.syscall_args
|
|
121
|
+
pending_syscalls[event.pid] = { syscall: syscall, args: syscall_args }
|
|
122
|
+
yield SyscallEvent.new(tracee: tracee, syscall: syscall, args: syscall_args, phase: :enter)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
resume_tracee_syscall(tracee, tracees: tracees, pending_syscalls: pending_syscalls)
|
|
127
|
+
end
|
|
128
|
+
ensure
|
|
129
|
+
tracees&.each_value { |tracee| safe_detach(tracee) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def resume_tracee_syscall(tracee, tracees:, pending_syscalls:)
|
|
133
|
+
tracee.syscall
|
|
134
|
+
rescue NoProcessError
|
|
135
|
+
pending_syscalls.delete(tracee.pid)
|
|
136
|
+
tracees.delete(tracee.pid)
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_seccomp_event(tracee)
|
|
141
|
+
metadata_flags = begin
|
|
142
|
+
tracee.seccomp_metadata_flag_names
|
|
143
|
+
rescue Error
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
SeccompEvent.new(
|
|
147
|
+
tracee: tracee,
|
|
148
|
+
syscall: tracee.current_syscall,
|
|
149
|
+
data: tracee.seccomp_data,
|
|
150
|
+
metadata_flags: metadata_flags
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def safe_detach(tracee)
|
|
155
|
+
begin
|
|
156
|
+
tracee&.detach
|
|
157
|
+
rescue Error, Errno::ESRCH
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
# Base error for ptrace operations.
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :errno, :request
|
|
7
|
+
|
|
8
|
+
def initialize(message = nil, errno: nil, request: nil)
|
|
9
|
+
@errno = errno
|
|
10
|
+
@request = request
|
|
11
|
+
|
|
12
|
+
if errno && request
|
|
13
|
+
super("ptrace(#{request}): #{message} (errno=#{errno})")
|
|
14
|
+
else
|
|
15
|
+
super(message)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Error raised for EPERM.
|
|
21
|
+
class PermissionError < Error; end
|
|
22
|
+
# Error raised for ESRCH.
|
|
23
|
+
class NoProcessError < Error; end
|
|
24
|
+
# Error raised for EBUSY.
|
|
25
|
+
class BusyError < Error; end
|
|
26
|
+
# Error raised for EINVAL.
|
|
27
|
+
class InvalidArgError < Error; end
|
|
28
|
+
# Unsupported architecture error.
|
|
29
|
+
class UnsupportedArchError < StandardError; end
|
|
30
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
# waitpid status wrapper with ptrace-specific helpers.
|
|
5
|
+
class Event
|
|
6
|
+
# ptrace event code to symbolic name map for inspect output.
|
|
7
|
+
EVENT_NAME_BY_CODE = {
|
|
8
|
+
Constants::PTRACE_EVENT_FORK => "fork",
|
|
9
|
+
Constants::PTRACE_EVENT_VFORK => "vfork",
|
|
10
|
+
Constants::PTRACE_EVENT_CLONE => "clone",
|
|
11
|
+
Constants::PTRACE_EVENT_EXEC => "exec",
|
|
12
|
+
Constants::PTRACE_EVENT_VFORK_DONE => "vfork_done",
|
|
13
|
+
Constants::PTRACE_EVENT_EXIT => "exit",
|
|
14
|
+
Constants::PTRACE_EVENT_SECCOMP => "seccomp",
|
|
15
|
+
Constants::PTRACE_EVENT_STOP => "stop"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :pid, :raw_status
|
|
19
|
+
|
|
20
|
+
def initialize(pid, raw_status)
|
|
21
|
+
@pid = Integer(pid)
|
|
22
|
+
@raw_status = Integer(raw_status)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def stopped?
|
|
27
|
+
WaitStatus.stopped?(raw_status)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def exited?
|
|
32
|
+
WaitStatus.exited?(raw_status)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def signaled?
|
|
37
|
+
WaitStatus.signaled?(raw_status)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def continued?
|
|
42
|
+
WaitStatus.continued?(raw_status)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Integer]
|
|
46
|
+
def stop_signal
|
|
47
|
+
WaitStatus.stop_signal(raw_status)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Integer]
|
|
51
|
+
def exit_status
|
|
52
|
+
WaitStatus.exit_status(raw_status)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Integer]
|
|
56
|
+
def term_signal
|
|
57
|
+
WaitStatus.term_signal(raw_status)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def syscall_stop?
|
|
62
|
+
stopped? && stop_signal == (Signal.list.fetch("TRAP") | 0x80)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Integer]
|
|
66
|
+
def event_code
|
|
67
|
+
(raw_status >> 16) & 0xFFFF
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fork_event?
|
|
71
|
+
event_code == Constants::PTRACE_EVENT_FORK
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clone_event?
|
|
75
|
+
event_code == Constants::PTRACE_EVENT_CLONE
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def vfork_event?
|
|
79
|
+
event_code == Constants::PTRACE_EVENT_VFORK
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def vfork_done_event?
|
|
83
|
+
event_code == Constants::PTRACE_EVENT_VFORK_DONE
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def exec_event?
|
|
87
|
+
event_code == Constants::PTRACE_EVENT_EXEC
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def exit_event?
|
|
91
|
+
event_code == Constants::PTRACE_EVENT_EXIT
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def seccomp_event?
|
|
95
|
+
event_code == Constants::PTRACE_EVENT_SECCOMP
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @return [Boolean]
|
|
99
|
+
def fork_like_event?
|
|
100
|
+
fork_event? || clone_event? || vfork_event?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [String]
|
|
104
|
+
def inspect
|
|
105
|
+
"#<#{self.class} pid=#{pid} status=0x#{raw_status.to_s(16)} #{state_summary}>"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def state_summary
|
|
111
|
+
return "state=syscall_stop" if syscall_stop?
|
|
112
|
+
return "state=exited(#{exit_status})" if exited?
|
|
113
|
+
return "state=continued" if continued?
|
|
114
|
+
if stopped?
|
|
115
|
+
event_name = EVENT_NAME_BY_CODE[event_code]
|
|
116
|
+
summary = "state=stopped(#{signal_label(stop_signal)})"
|
|
117
|
+
return "#{summary} event=#{event_name}" if event_name
|
|
118
|
+
|
|
119
|
+
return summary
|
|
120
|
+
end
|
|
121
|
+
return "state=signaled(#{signal_label(term_signal)})" if signaled?
|
|
122
|
+
|
|
123
|
+
"state=unknown"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def signal_label(number)
|
|
127
|
+
signal_name = Signal.signame(number)
|
|
128
|
+
signal_name = "SIG#{signal_name}" unless signal_name.start_with?("SIG")
|
|
129
|
+
signal_name
|
|
130
|
+
rescue ArgumentError
|
|
131
|
+
"SIG#{number}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|