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,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fcntl"
|
|
4
|
+
|
|
5
|
+
module Rptrace
|
|
6
|
+
# Renderable syscall event (enter/exit).
|
|
7
|
+
class SyscallEvent
|
|
8
|
+
# Map of errno number to symbolic name (e.g., 2 => "ENOENT").
|
|
9
|
+
ERRNO_NAME_BY_NUMBER = Errno.constants.each_with_object({}) do |const_name, map|
|
|
10
|
+
errno_class = Errno.const_get(const_name)
|
|
11
|
+
next unless errno_class.is_a?(Class) && errno_class < SystemCallError
|
|
12
|
+
next unless errno_class.const_defined?(:Errno)
|
|
13
|
+
|
|
14
|
+
map[errno_class::Errno] ||= const_name.to_s
|
|
15
|
+
rescue NameError
|
|
16
|
+
next
|
|
17
|
+
end.freeze
|
|
18
|
+
# Linux kernel upper bound for -errno syscall return convention.
|
|
19
|
+
LINUX_MAX_ERRNO = 4095
|
|
20
|
+
# Access mode mask for open/openat flags.
|
|
21
|
+
OPEN_ACCESS_MASK = Fcntl.const_defined?(:O_ACCMODE) ? Fcntl::O_ACCMODE : 0x3
|
|
22
|
+
# Mapping of open access mode value to symbolic name.
|
|
23
|
+
OPEN_ACCESS_NAMES = {
|
|
24
|
+
(Fcntl.const_defined?(:O_RDONLY) ? Fcntl::O_RDONLY : 0) => "O_RDONLY",
|
|
25
|
+
(Fcntl.const_defined?(:O_WRONLY) ? Fcntl::O_WRONLY : 1) => "O_WRONLY",
|
|
26
|
+
(Fcntl.const_defined?(:O_RDWR) ? Fcntl::O_RDWR : 2) => "O_RDWR"
|
|
27
|
+
}.freeze
|
|
28
|
+
# Mapping of open/openat modifier bits to symbolic names.
|
|
29
|
+
OPEN_FLAG_NAMES = %i[
|
|
30
|
+
O_APPEND O_ASYNC O_CLOEXEC O_CREAT O_DIRECT O_DIRECTORY O_DSYNC O_EXCL O_LARGEFILE
|
|
31
|
+
O_NOATIME O_NOCTTY O_NOFOLLOW O_NONBLOCK O_PATH O_SYNC O_TMPFILE O_TRUNC
|
|
32
|
+
].each_with_object({}) do |const_name, map|
|
|
33
|
+
next unless Fcntl.const_defined?(const_name)
|
|
34
|
+
|
|
35
|
+
value = Fcntl.const_get(const_name)
|
|
36
|
+
next unless value.is_a?(Integer) && value.positive?
|
|
37
|
+
|
|
38
|
+
map[value] = const_name.to_s
|
|
39
|
+
end.freeze
|
|
40
|
+
# Mapping of PROT_* values used by mmap/mprotect.
|
|
41
|
+
PROT_NAMES = %i[
|
|
42
|
+
PROT_EXEC PROT_GROWSDOWN PROT_GROWSUP PROT_NONE PROT_READ PROT_SEM PROT_WRITE
|
|
43
|
+
].each_with_object({}) do |const_name, map|
|
|
44
|
+
next unless Fcntl.const_defined?(const_name)
|
|
45
|
+
|
|
46
|
+
value = Fcntl.const_get(const_name)
|
|
47
|
+
next unless value.is_a?(Integer)
|
|
48
|
+
|
|
49
|
+
map[value] = const_name.to_s
|
|
50
|
+
end.freeze
|
|
51
|
+
# Bit mask for MAP_* type field.
|
|
52
|
+
MAP_TYPE_MASK = Fcntl.const_defined?(:MAP_TYPE) ? Fcntl::MAP_TYPE : nil
|
|
53
|
+
# Mapping of mmap type flags.
|
|
54
|
+
MAP_TYPE_NAMES = %i[
|
|
55
|
+
MAP_PRIVATE MAP_SHARED MAP_SHARED_VALIDATE
|
|
56
|
+
].each_with_object({}) do |const_name, map|
|
|
57
|
+
next unless Fcntl.const_defined?(const_name)
|
|
58
|
+
|
|
59
|
+
value = Fcntl.const_get(const_name)
|
|
60
|
+
next unless value.is_a?(Integer)
|
|
61
|
+
|
|
62
|
+
map[value] = const_name.to_s
|
|
63
|
+
end.freeze
|
|
64
|
+
# Mapping of additional mmap modifier bits.
|
|
65
|
+
MAP_FLAG_NAMES = %i[
|
|
66
|
+
MAP_32BIT MAP_ANONYMOUS MAP_ANON MAP_DENYWRITE MAP_EXECUTABLE MAP_FILE
|
|
67
|
+
MAP_FIXED MAP_FIXED_NOREPLACE MAP_GROWSDOWN MAP_HUGETLB MAP_LOCKED MAP_NONBLOCK
|
|
68
|
+
MAP_NORESERVE MAP_POPULATE MAP_STACK MAP_SYNC MAP_UNINITIALIZED
|
|
69
|
+
].each_with_object({}) do |const_name, map|
|
|
70
|
+
next unless Fcntl.const_defined?(const_name)
|
|
71
|
+
|
|
72
|
+
value = Fcntl.const_get(const_name)
|
|
73
|
+
next unless value.is_a?(Integer) && value.positive?
|
|
74
|
+
|
|
75
|
+
map[value] ||= const_name.to_s
|
|
76
|
+
end.freeze
|
|
77
|
+
# Mapping of clone(2) flag bits in the upper bits of clone flags argument.
|
|
78
|
+
CLONE_FLAG_NAMES = {
|
|
79
|
+
0x0000_0100 => "CLONE_VM",
|
|
80
|
+
0x0000_0200 => "CLONE_FS",
|
|
81
|
+
0x0000_0400 => "CLONE_FILES",
|
|
82
|
+
0x0000_0800 => "CLONE_SIGHAND",
|
|
83
|
+
0x0000_1000 => "CLONE_PIDFD",
|
|
84
|
+
0x0000_2000 => "CLONE_PTRACE",
|
|
85
|
+
0x0000_4000 => "CLONE_VFORK",
|
|
86
|
+
0x0000_8000 => "CLONE_PARENT",
|
|
87
|
+
0x0001_0000 => "CLONE_THREAD",
|
|
88
|
+
0x0002_0000 => "CLONE_NEWNS",
|
|
89
|
+
0x0004_0000 => "CLONE_SYSVSEM",
|
|
90
|
+
0x0008_0000 => "CLONE_SETTLS",
|
|
91
|
+
0x0010_0000 => "CLONE_PARENT_SETTID",
|
|
92
|
+
0x0020_0000 => "CLONE_CHILD_CLEARTID",
|
|
93
|
+
0x0040_0000 => "CLONE_DETACHED",
|
|
94
|
+
0x0080_0000 => "CLONE_UNTRACED",
|
|
95
|
+
0x0100_0000 => "CLONE_CHILD_SETTID",
|
|
96
|
+
0x0200_0000 => "CLONE_NEWCGROUP",
|
|
97
|
+
0x0400_0000 => "CLONE_NEWUTS",
|
|
98
|
+
0x0800_0000 => "CLONE_NEWIPC",
|
|
99
|
+
0x1000_0000 => "CLONE_NEWUSER",
|
|
100
|
+
0x2000_0000 => "CLONE_NEWPID",
|
|
101
|
+
0x4000_0000 => "CLONE_NEWNET",
|
|
102
|
+
0x8000_0000 => "CLONE_IO"
|
|
103
|
+
}.freeze
|
|
104
|
+
# Mapping of wait4(2) option bits.
|
|
105
|
+
WAIT_OPTION_NAMES = {
|
|
106
|
+
Constants::WNOHANG => "WNOHANG",
|
|
107
|
+
Constants::WUNTRACED => "WUNTRACED",
|
|
108
|
+
Constants::WCONTINUED => "WCONTINUED",
|
|
109
|
+
Constants::WALL => "__WALL"
|
|
110
|
+
}.freeze
|
|
111
|
+
|
|
112
|
+
attr_reader :tracee, :syscall, :args, :return_value, :phase
|
|
113
|
+
|
|
114
|
+
def initialize(tracee:, syscall:, args:, phase:, return_value: nil)
|
|
115
|
+
@tracee = tracee
|
|
116
|
+
@syscall = syscall
|
|
117
|
+
@args = args
|
|
118
|
+
@phase = phase
|
|
119
|
+
@return_value = return_value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Boolean]
|
|
123
|
+
def enter?
|
|
124
|
+
phase == :enter
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [Boolean]
|
|
128
|
+
def exit?
|
|
129
|
+
phase == :exit
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [String]
|
|
133
|
+
def to_s
|
|
134
|
+
call = "#{syscall.name}(#{formatted_args})"
|
|
135
|
+
return "#{call} ..." if enter?
|
|
136
|
+
|
|
137
|
+
"#{call} = #{formatted_return_value}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [String]
|
|
141
|
+
def formatted_args
|
|
142
|
+
args.each_with_index.map do |value, index|
|
|
143
|
+
type = syscall.arg_types.fetch(index, nil)
|
|
144
|
+
format_argument(type, value, index)
|
|
145
|
+
end.join(", ")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [String]
|
|
149
|
+
def formatted_return_value
|
|
150
|
+
return "?" if return_value.nil?
|
|
151
|
+
return return_value.to_s unless syscall_error_code?(return_value)
|
|
152
|
+
|
|
153
|
+
errno = -return_value
|
|
154
|
+
name = ERRNO_NAME_BY_NUMBER.fetch(errno, "ERRNO_#{errno}")
|
|
155
|
+
message = SystemCallError.new(errno).message
|
|
156
|
+
"-1 #{name} (#{message})"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def format_argument(type, value, index)
|
|
162
|
+
case type
|
|
163
|
+
when :str
|
|
164
|
+
format_string_pointer(value)
|
|
165
|
+
when :ptr, :buf
|
|
166
|
+
format_pointer(value)
|
|
167
|
+
when :flags
|
|
168
|
+
format_flags(value, index: index)
|
|
169
|
+
when :mode
|
|
170
|
+
format_mode(value)
|
|
171
|
+
when :fd, :int, :uint, :size, :pid
|
|
172
|
+
Integer(value).to_s
|
|
173
|
+
else
|
|
174
|
+
value.inspect
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def format_string_pointer(value)
|
|
179
|
+
return "NULL" if pointer_null?(value)
|
|
180
|
+
|
|
181
|
+
tracee.memory.read_string(Integer(value)).inspect
|
|
182
|
+
rescue Rptrace::Error, StandardError
|
|
183
|
+
format_pointer(value)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def format_pointer(value)
|
|
187
|
+
return "NULL" if pointer_null?(value)
|
|
188
|
+
|
|
189
|
+
format("0x%x", Integer(value))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def format_flags(value, index:)
|
|
193
|
+
number = Integer(value)
|
|
194
|
+
|
|
195
|
+
if open_flags_argument?(index)
|
|
196
|
+
decode_open_flags(number)
|
|
197
|
+
elsif mmap_prot_argument?(index)
|
|
198
|
+
decode_mmap_prot(number)
|
|
199
|
+
elsif mmap_flags_argument?(index)
|
|
200
|
+
decode_mmap_flags(number)
|
|
201
|
+
elsif clone_flags_argument?(index)
|
|
202
|
+
decode_clone_flags(number)
|
|
203
|
+
elsif wait_options_argument?(index)
|
|
204
|
+
decode_wait_options(number)
|
|
205
|
+
else
|
|
206
|
+
format("0x%x", number)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def format_mode(value)
|
|
211
|
+
number = Integer(value)
|
|
212
|
+
return "0" if number.zero?
|
|
213
|
+
|
|
214
|
+
format("0%o", number)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def open_flags_argument?(index)
|
|
218
|
+
return false unless %i[open openat].include?(syscall.name)
|
|
219
|
+
|
|
220
|
+
flag_arg_name = syscall.arg_names.fetch(index, nil)
|
|
221
|
+
flag_arg_name == :flags
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def mmap_prot_argument?(index)
|
|
225
|
+
return false unless syscall.name == :mmap
|
|
226
|
+
|
|
227
|
+
syscall.arg_names.fetch(index, nil) == :prot
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def mmap_flags_argument?(index)
|
|
231
|
+
return false unless syscall.name == :mmap
|
|
232
|
+
|
|
233
|
+
syscall.arg_names.fetch(index, nil) == :flags
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def clone_flags_argument?(index)
|
|
237
|
+
return false unless syscall.name == :clone
|
|
238
|
+
|
|
239
|
+
syscall.arg_names.fetch(index, nil) == :flags
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def wait_options_argument?(index)
|
|
243
|
+
return false unless syscall.name == :wait4
|
|
244
|
+
|
|
245
|
+
syscall.arg_names.fetch(index, nil) == :options
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def decode_open_flags(value)
|
|
249
|
+
names = []
|
|
250
|
+
|
|
251
|
+
access = value & OPEN_ACCESS_MASK
|
|
252
|
+
names << OPEN_ACCESS_NAMES.fetch(access, format("0x%x", access))
|
|
253
|
+
|
|
254
|
+
remaining = value & ~OPEN_ACCESS_MASK
|
|
255
|
+
OPEN_FLAG_NAMES.keys.sort.reverse_each do |bit|
|
|
256
|
+
next if bit.zero?
|
|
257
|
+
next unless (remaining & bit) == bit
|
|
258
|
+
|
|
259
|
+
names << OPEN_FLAG_NAMES.fetch(bit)
|
|
260
|
+
remaining &= ~bit
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
names << format("0x%x", remaining) unless remaining.zero?
|
|
264
|
+
names.join("|")
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def decode_mmap_prot(value)
|
|
268
|
+
return PROT_NAMES.fetch(0, "0") if value.zero?
|
|
269
|
+
|
|
270
|
+
names = []
|
|
271
|
+
remaining = value
|
|
272
|
+
PROT_NAMES.keys.sort.each do |bit|
|
|
273
|
+
next if bit.zero?
|
|
274
|
+
next unless (remaining & bit) == bit
|
|
275
|
+
|
|
276
|
+
names << PROT_NAMES.fetch(bit)
|
|
277
|
+
remaining &= ~bit
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
names << format("0x%x", remaining) unless remaining.zero?
|
|
281
|
+
names.empty? ? format("0x%x", value) : names.join("|")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def decode_mmap_flags(value)
|
|
285
|
+
names = []
|
|
286
|
+
remaining = value
|
|
287
|
+
|
|
288
|
+
if MAP_TYPE_MASK
|
|
289
|
+
map_type = value & MAP_TYPE_MASK
|
|
290
|
+
names << MAP_TYPE_NAMES.fetch(map_type, format("0x%x", map_type)) unless map_type.zero?
|
|
291
|
+
remaining &= ~MAP_TYPE_MASK
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
MAP_FLAG_NAMES.keys.sort.each do |bit|
|
|
295
|
+
next unless (remaining & bit) == bit
|
|
296
|
+
|
|
297
|
+
names << MAP_FLAG_NAMES.fetch(bit)
|
|
298
|
+
remaining &= ~bit
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
names << format("0x%x", remaining) unless remaining.zero?
|
|
302
|
+
names.empty? ? format("0x%x", value) : names.join("|")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def decode_clone_flags(value)
|
|
306
|
+
names = []
|
|
307
|
+
exit_signal = value & 0xFF
|
|
308
|
+
remaining = value & ~0xFF
|
|
309
|
+
|
|
310
|
+
CLONE_FLAG_NAMES.keys.sort.each do |bit|
|
|
311
|
+
next unless (remaining & bit) == bit
|
|
312
|
+
|
|
313
|
+
names << CLONE_FLAG_NAMES.fetch(bit)
|
|
314
|
+
remaining &= ~bit
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
names << format_clone_exit_signal(exit_signal) unless exit_signal.zero?
|
|
318
|
+
names << format("0x%x", remaining) unless remaining.zero?
|
|
319
|
+
names.empty? ? "0" : names.join("|")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def format_clone_exit_signal(signal)
|
|
323
|
+
signal_name = Signal.signame(signal)
|
|
324
|
+
signal_name.start_with?("SIG") ? signal_name : "SIG#{signal_name}"
|
|
325
|
+
rescue ArgumentError
|
|
326
|
+
signal.to_s
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def decode_wait_options(value)
|
|
330
|
+
return "0" if value.zero?
|
|
331
|
+
|
|
332
|
+
names = []
|
|
333
|
+
remaining = value
|
|
334
|
+
WAIT_OPTION_NAMES.keys.sort.each do |bit|
|
|
335
|
+
next unless (remaining & bit) == bit
|
|
336
|
+
|
|
337
|
+
names << WAIT_OPTION_NAMES.fetch(bit)
|
|
338
|
+
remaining &= ~bit
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
names << format("0x%x", remaining) unless remaining.zero?
|
|
342
|
+
names.empty? ? format("0x%x", value) : names.join("|")
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def pointer_null?(value)
|
|
346
|
+
Integer(value).zero?
|
|
347
|
+
rescue StandardError
|
|
348
|
+
false
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def syscall_error_code?(value)
|
|
352
|
+
value.is_a?(Integer) && value.negative? && (-value) <= LINUX_MAX_ERRNO
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
module SyscallTable
|
|
5
|
+
# aarch64 syscall table.
|
|
6
|
+
module AARCH64
|
|
7
|
+
# @return [Hash{Integer => Rptrace::Syscall::SyscallInfo}]
|
|
8
|
+
TABLE = {
|
|
9
|
+
56 => Syscall::SyscallInfo.new(number: 56, name: :openat, arg_names: %i[dirfd pathname flags mode], arg_types: %i[fd str flags mode]),
|
|
10
|
+
57 => Syscall::SyscallInfo.new(number: 57, name: :close, arg_names: %i[fd], arg_types: %i[fd]),
|
|
11
|
+
63 => Syscall::SyscallInfo.new(number: 63, name: :read, arg_names: %i[fd buf count], arg_types: %i[fd buf size]),
|
|
12
|
+
64 => Syscall::SyscallInfo.new(number: 64, name: :write, arg_names: %i[fd buf count], arg_types: %i[fd buf size]),
|
|
13
|
+
93 => Syscall::SyscallInfo.new(number: 93, name: :exit, arg_names: %i[status], arg_types: %i[int]),
|
|
14
|
+
94 => Syscall::SyscallInfo.new(number: 94, name: :exit_group, arg_names: %i[status], arg_types: %i[int]),
|
|
15
|
+
221 => Syscall::SyscallInfo.new(number: 221, name: :execve, arg_names: %i[filename argv envp], arg_types: %i[str ptr ptr])
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# @return [Hash{Symbol => Rptrace::Syscall::SyscallInfo}]
|
|
19
|
+
BY_NAME = TABLE.each_with_object({}) do |(_number, info), map|
|
|
20
|
+
map[info.name] = info
|
|
21
|
+
end.freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
module SyscallTable
|
|
5
|
+
# Generates syscall table Ruby files from Linux unistd headers.
|
|
6
|
+
module Generator
|
|
7
|
+
# Raised when syscall header cannot be resolved for an architecture.
|
|
8
|
+
class HeaderNotFoundError < ArgumentError; end
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Header lookup and output mapping by architecture.
|
|
13
|
+
ARCH_CONFIG = {
|
|
14
|
+
x86_64: {
|
|
15
|
+
module_name: "X86_64",
|
|
16
|
+
output_path: "lib/rptrace/syscall_table/x86_64.rb",
|
|
17
|
+
env_key: "PTRACE_SYSCALL_HEADER_X86_64",
|
|
18
|
+
header_candidates: [
|
|
19
|
+
"/usr/include/x86_64-linux-gnu/asm/unistd_64.h",
|
|
20
|
+
"/usr/include/asm/unistd_64.h",
|
|
21
|
+
"/usr/include/x86_64-linux-gnu/asm/unistd.h"
|
|
22
|
+
].freeze
|
|
23
|
+
}.freeze,
|
|
24
|
+
aarch64: {
|
|
25
|
+
module_name: "AARCH64",
|
|
26
|
+
output_path: "lib/rptrace/syscall_table/aarch64.rb",
|
|
27
|
+
env_key: "PTRACE_SYSCALL_HEADER_AARCH64",
|
|
28
|
+
header_candidates: [
|
|
29
|
+
"/usr/include/aarch64-linux-gnu/asm/unistd.h",
|
|
30
|
+
"/usr/aarch64-linux-gnu/include/asm/unistd.h",
|
|
31
|
+
"/usr/include/asm-generic/unistd.h"
|
|
32
|
+
].freeze
|
|
33
|
+
}.freeze
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# Matches numeric __NR_* macro definitions.
|
|
37
|
+
DEFINE_REGEX = /^\s*#\s*define\s+__NR(?:3264)?_([a-zA-Z0-9_]+)\s+([0-9]+)\b/.freeze
|
|
38
|
+
|
|
39
|
+
# Generates tables for all configured architectures.
|
|
40
|
+
#
|
|
41
|
+
# @param root_dir [String]
|
|
42
|
+
# @param arches [Array<Symbol>]
|
|
43
|
+
# @param skip_missing [Boolean] skip architectures without headers
|
|
44
|
+
# @return [Array<Hash>]
|
|
45
|
+
def generate_all(root_dir: Dir.pwd, arches: ARCH_CONFIG.keys, skip_missing: false)
|
|
46
|
+
generated = []
|
|
47
|
+
|
|
48
|
+
arches.each do |arch|
|
|
49
|
+
begin
|
|
50
|
+
generated << generate_for(arch, root_dir: root_dir)
|
|
51
|
+
rescue HeaderNotFoundError
|
|
52
|
+
raise unless skip_missing
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
generated
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Generates tables and reports skipped architectures.
|
|
60
|
+
#
|
|
61
|
+
# @param root_dir [String]
|
|
62
|
+
# @param arches [Array<Symbol>]
|
|
63
|
+
# @return [Hash]
|
|
64
|
+
def generate_available(root_dir: Dir.pwd, arches: ARCH_CONFIG.keys)
|
|
65
|
+
generated = []
|
|
66
|
+
skipped = []
|
|
67
|
+
|
|
68
|
+
arches.each do |arch|
|
|
69
|
+
begin
|
|
70
|
+
generated << generate_for(arch, root_dir: root_dir)
|
|
71
|
+
rescue HeaderNotFoundError => e
|
|
72
|
+
skipped << { arch: arch.to_sym, reason: e.message }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
{ generated: generated, skipped: skipped }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Generates one syscall table file.
|
|
80
|
+
#
|
|
81
|
+
# @param arch [Symbol]
|
|
82
|
+
# @param root_dir [String]
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
def generate_for(arch, root_dir: Dir.pwd)
|
|
85
|
+
config = ARCH_CONFIG.fetch(arch.to_sym) do
|
|
86
|
+
raise ArgumentError, "unsupported arch: #{arch}"
|
|
87
|
+
end
|
|
88
|
+
header_path = resolve_header_path(config)
|
|
89
|
+
entries = parse_header(File.read(header_path))
|
|
90
|
+
raise ArgumentError, "no syscall entries found in #{header_path}" if entries.empty?
|
|
91
|
+
|
|
92
|
+
output_path = File.expand_path(config.fetch(:output_path), root_dir)
|
|
93
|
+
File.write(output_path, render_table(module_name: config.fetch(:module_name), entries: entries))
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
arch: arch.to_sym,
|
|
97
|
+
header_path: header_path,
|
|
98
|
+
output_path: output_path,
|
|
99
|
+
entries_count: entries.size
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parses #define __NR_* entries from header content.
|
|
104
|
+
#
|
|
105
|
+
# @param content [String]
|
|
106
|
+
# @return [Array<(Integer, Symbol)>]
|
|
107
|
+
def parse_header(content)
|
|
108
|
+
by_number = {}
|
|
109
|
+
|
|
110
|
+
content.each_line do |line|
|
|
111
|
+
match = line.match(DEFINE_REGEX)
|
|
112
|
+
next unless match
|
|
113
|
+
|
|
114
|
+
name = match[1].to_sym
|
|
115
|
+
number = Integer(match[2], 10)
|
|
116
|
+
by_number[number] ||= name
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
by_number.sort_by { |number, _name| number }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Renders a syscall table Ruby source.
|
|
123
|
+
#
|
|
124
|
+
# @param module_name [String]
|
|
125
|
+
# @param entries [Array<(Integer, Symbol)>]
|
|
126
|
+
# @return [String]
|
|
127
|
+
def render_table(module_name:, entries:)
|
|
128
|
+
table_lines = entries.map do |number, name|
|
|
129
|
+
" #{number} => Syscall::SyscallInfo.new(number: #{number}, name: :#{name}, arg_names: [], arg_types: [])"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
<<~RUBY
|
|
133
|
+
# frozen_string_literal: true
|
|
134
|
+
|
|
135
|
+
module Rptrace
|
|
136
|
+
module SyscallTable
|
|
137
|
+
module #{module_name}
|
|
138
|
+
TABLE = {
|
|
139
|
+
#{table_lines.join(",\n")}
|
|
140
|
+
}.freeze
|
|
141
|
+
|
|
142
|
+
BY_NAME = TABLE.each_with_object({}) do |(_number, info), map|
|
|
143
|
+
map[info.name] = info
|
|
144
|
+
end.freeze
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
RUBY
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def resolve_header_path(config)
|
|
152
|
+
env_key = config.fetch(:env_key)
|
|
153
|
+
env_value = ENV[env_key]
|
|
154
|
+
|
|
155
|
+
if env_value && !env_value.empty?
|
|
156
|
+
expanded = File.expand_path(env_value)
|
|
157
|
+
return expanded if File.file?(expanded)
|
|
158
|
+
|
|
159
|
+
raise HeaderNotFoundError, "header path from #{env_key} does not exist: #{expanded}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
config.fetch(:header_candidates).each do |candidate|
|
|
163
|
+
expanded = File.expand_path(candidate)
|
|
164
|
+
return expanded if File.file?(expanded)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
raise HeaderNotFoundError, "no syscall header found. set #{env_key}=<path-to-header>"
|
|
168
|
+
end
|
|
169
|
+
private_class_method :resolve_header_path
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
# Built-in syscall number table modules.
|
|
5
|
+
module SyscallTable
|
|
6
|
+
# x86_64 syscall table.
|
|
7
|
+
module X86_64
|
|
8
|
+
# @return [Hash{Integer => Rptrace::Syscall::SyscallInfo}]
|
|
9
|
+
TABLE = {
|
|
10
|
+
0 => Syscall::SyscallInfo.new(number: 0, name: :read, arg_names: [], arg_types: []),
|
|
11
|
+
1 => Syscall::SyscallInfo.new(number: 1, name: :write, arg_names: [], arg_types: []),
|
|
12
|
+
2 => Syscall::SyscallInfo.new(number: 2, name: :open, arg_names: [], arg_types: []),
|
|
13
|
+
3 => Syscall::SyscallInfo.new(number: 3, name: :close, arg_names: [], arg_types: []),
|
|
14
|
+
4 => Syscall::SyscallInfo.new(number: 4, name: :stat, arg_names: [], arg_types: []),
|
|
15
|
+
5 => Syscall::SyscallInfo.new(number: 5, name: :fstat, arg_names: [], arg_types: []),
|
|
16
|
+
6 => Syscall::SyscallInfo.new(number: 6, name: :lstat, arg_names: [], arg_types: []),
|
|
17
|
+
7 => Syscall::SyscallInfo.new(number: 7, name: :poll, arg_names: [], arg_types: []),
|
|
18
|
+
8 => Syscall::SyscallInfo.new(number: 8, name: :lseek, arg_names: [], arg_types: []),
|
|
19
|
+
9 => Syscall::SyscallInfo.new(number: 9, name: :mmap, arg_names: [], arg_types: []),
|
|
20
|
+
10 => Syscall::SyscallInfo.new(number: 10, name: :mprotect, arg_names: [], arg_types: []),
|
|
21
|
+
11 => Syscall::SyscallInfo.new(number: 11, name: :munmap, arg_names: [], arg_types: []),
|
|
22
|
+
12 => Syscall::SyscallInfo.new(number: 12, name: :brk, arg_names: [], arg_types: []),
|
|
23
|
+
16 => Syscall::SyscallInfo.new(number: 16, name: :ioctl, arg_names: [], arg_types: []),
|
|
24
|
+
21 => Syscall::SyscallInfo.new(number: 21, name: :access, arg_names: [], arg_types: []),
|
|
25
|
+
22 => Syscall::SyscallInfo.new(number: 22, name: :pipe, arg_names: [], arg_types: []),
|
|
26
|
+
23 => Syscall::SyscallInfo.new(number: 23, name: :select, arg_names: [], arg_types: []),
|
|
27
|
+
32 => Syscall::SyscallInfo.new(number: 32, name: :dup, arg_names: [], arg_types: []),
|
|
28
|
+
33 => Syscall::SyscallInfo.new(number: 33, name: :dup2, arg_names: [], arg_types: []),
|
|
29
|
+
41 => Syscall::SyscallInfo.new(number: 41, name: :socket, arg_names: [], arg_types: []),
|
|
30
|
+
42 => Syscall::SyscallInfo.new(number: 42, name: :connect, arg_names: [], arg_types: []),
|
|
31
|
+
43 => Syscall::SyscallInfo.new(number: 43, name: :accept, arg_names: [], arg_types: []),
|
|
32
|
+
44 => Syscall::SyscallInfo.new(number: 44, name: :sendto, arg_names: [], arg_types: []),
|
|
33
|
+
45 => Syscall::SyscallInfo.new(number: 45, name: :recvfrom, arg_names: [], arg_types: []),
|
|
34
|
+
48 => Syscall::SyscallInfo.new(number: 48, name: :shutdown, arg_names: [], arg_types: []),
|
|
35
|
+
49 => Syscall::SyscallInfo.new(number: 49, name: :bind, arg_names: [], arg_types: []),
|
|
36
|
+
50 => Syscall::SyscallInfo.new(number: 50, name: :listen, arg_names: [], arg_types: []),
|
|
37
|
+
56 => Syscall::SyscallInfo.new(number: 56, name: :clone, arg_names: [], arg_types: []),
|
|
38
|
+
57 => Syscall::SyscallInfo.new(number: 57, name: :fork, arg_names: [], arg_types: []),
|
|
39
|
+
58 => Syscall::SyscallInfo.new(number: 58, name: :vfork, arg_names: [], arg_types: []),
|
|
40
|
+
59 => Syscall::SyscallInfo.new(number: 59, name: :execve, arg_names: [], arg_types: []),
|
|
41
|
+
60 => Syscall::SyscallInfo.new(number: 60, name: :exit, arg_names: [], arg_types: []),
|
|
42
|
+
61 => Syscall::SyscallInfo.new(number: 61, name: :wait4, arg_names: [], arg_types: []),
|
|
43
|
+
62 => Syscall::SyscallInfo.new(number: 62, name: :kill, arg_names: [], arg_types: []),
|
|
44
|
+
72 => Syscall::SyscallInfo.new(number: 72, name: :fcntl, arg_names: [], arg_types: []),
|
|
45
|
+
73 => Syscall::SyscallInfo.new(number: 73, name: :flock, arg_names: [], arg_types: []),
|
|
46
|
+
74 => Syscall::SyscallInfo.new(number: 74, name: :fsync, arg_names: [], arg_types: []),
|
|
47
|
+
231 => Syscall::SyscallInfo.new(number: 231, name: :exit_group, arg_names: [], arg_types: []),
|
|
48
|
+
257 => Syscall::SyscallInfo.new(number: 257, name: :openat, arg_names: [], arg_types: []),
|
|
49
|
+
262 => Syscall::SyscallInfo.new(number: 262, name: :newfstatat, arg_names: [], arg_types: []),
|
|
50
|
+
267 => Syscall::SyscallInfo.new(number: 267, name: :readlinkat, arg_names: [], arg_types: []),
|
|
51
|
+
269 => Syscall::SyscallInfo.new(number: 269, name: :faccessat, arg_names: [], arg_types: [])
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# @return [Hash{Symbol => Rptrace::Syscall::SyscallInfo}]
|
|
55
|
+
BY_NAME = TABLE.each_with_object({}) do |(_number, info), map|
|
|
56
|
+
map[info.name] = info
|
|
57
|
+
end.freeze
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|