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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: db5900c94042eb61d01de6b3c3394d23833fde0ccf3c23a7974618d52686cf68
|
|
4
|
+
data.tar.gz: a34a2007bca148522855b1e5019c1c6d5975440621a9132ec50d6a71f6f04c20
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 748068d4a08b509c8ccd31bf30c5643236b7b8d9df3eaaf737c251c67173c162629a0e5a3780efb0a69037a9bee66de38f0a4905a0bf63cc05f91e971cd6aef8
|
|
7
|
+
data.tar.gz: 4a87fb8ea9fa2c52f2d36b1889d2d124f5bc53728688a351b2e5c3c5c9785584967b80d37684a410773fb360134cb6cd1e2c73c9c1dccb8431bf1a61eddd1cc3
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# rptrace
|
|
2
|
+
|
|
3
|
+
`rptrace` is a Ruby wrapper for Linux `ptrace(2)` focused on building tracers and debugger-like tooling with a Ruby-friendly API.
|
|
4
|
+
|
|
5
|
+
## Overview and Motivation
|
|
6
|
+
|
|
7
|
+
Linux `ptrace(2)` is powerful but low-level. This gem wraps process control, register/memory access, and syscall decoding behind a small Ruby API so you can build:
|
|
8
|
+
|
|
9
|
+
- `strace`-like tools
|
|
10
|
+
- process instrumentation utilities
|
|
11
|
+
- debugger-oriented experiments
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Top-level namespace is `Rptrace` (no `Rptrace::Ruby` nesting)
|
|
16
|
+
- `Tracee` API for `spawn`, `attach`, `cont`, `syscall`, `detach`, and `wait`
|
|
17
|
+
- Register and memory wrappers (`Registers`, `Memory`)
|
|
18
|
+
- `/proc/<pid>/maps` parser (`ProcMaps`, `Tracee#memory_maps`)
|
|
19
|
+
- Software breakpoints on x86_64 (`Tracee#set_breakpoint`, `remove_breakpoint`)
|
|
20
|
+
- Syscall lookup (`Rptrace::Syscall`) for `x86_64`/`aarch64`
|
|
21
|
+
- High-level tracing helper `Rptrace.strace` (`follow_children` / `yield_seccomp` supported)
|
|
22
|
+
- ptrace event helpers (`Tracee#event_message`, `Tracee#seccomp_data`, `Tracee#seccomp_metadata`, `Tracee#seccomp_filter`)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Add this line to your application's Gemfile:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem "rptrace"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
And then execute:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bundle install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require "rptrace"
|
|
42
|
+
|
|
43
|
+
Rptrace.strace("/bin/ls", "-la", "/tmp") do |event|
|
|
44
|
+
next unless event.exit?
|
|
45
|
+
|
|
46
|
+
puts event
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Follow child processes/threads (clone/fork/vfork):
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
Rptrace.strace("/usr/bin/ruby", "-e", "pid = fork { sleep 0.1 }; Process.wait(pid)", follow_children: true) do |event|
|
|
54
|
+
next unless event.enter?
|
|
55
|
+
puts "pid=#{event.tracee.pid} #{event.syscall.name}"
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Include seccomp stop events in trace stream:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
Rptrace.strace("/bin/ls", "/tmp", yield_seccomp: true) do |event|
|
|
63
|
+
case event
|
|
64
|
+
when Rptrace::SyscallEvent
|
|
65
|
+
puts event if event.exit?
|
|
66
|
+
when Rptrace::SeccompEvent
|
|
67
|
+
warn event.to_s
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Set and clear a software breakpoint (x86_64):
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
tracee = Rptrace::Tracee.attach(target_pid)
|
|
76
|
+
bp = tracee.set_breakpoint(0x401000)
|
|
77
|
+
# ...
|
|
78
|
+
bp.restore
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Inspect seccomp filter metadata and decoded BPF instructions:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
tracee = Rptrace::Tracee.attach(target_pid)
|
|
85
|
+
tracee.enable_seccomp_events!
|
|
86
|
+
supported = tracee.seccomp_supported?
|
|
87
|
+
available = tracee.seccomp_filter_available?(index: 0)
|
|
88
|
+
meta = tracee.seccomp_metadata(index: 0) # => { filter_off: 0, flags: ... }
|
|
89
|
+
flag_names = tracee.seccomp_metadata_flag_names(index: 0) # => [:tsync, :log, ...]
|
|
90
|
+
insns = tracee.seccomp_filter(index: 0) # => [{ code:, jt:, jf:, k: }, ...]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Permission Guide
|
|
94
|
+
|
|
95
|
+
`ptrace` requires privilege on Linux:
|
|
96
|
+
|
|
97
|
+
- run as `root`, or
|
|
98
|
+
- run with `CAP_SYS_PTRACE`, and
|
|
99
|
+
- ensure Yama policy allows tracing (`/proc/sys/kernel/yama/ptrace_scope`)
|
|
100
|
+
|
|
101
|
+
Integration specs are opt-in and require:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
PTRACE_RUN_INTEGRATION=1 bundle exec rspec spec/integration
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
You can inspect local ptrace capability setup from Ruby:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
diagnostics = Rptrace.ptrace_permissions
|
|
111
|
+
puts diagnostics # => { ptrace_privileged:, cap_sys_ptrace:, yama_ptrace_scope:, hints: [...] }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Fail fast with an actionable permission error:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
Rptrace.ensure_ptrace_privileged!(request: :attach)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
- `examples/simple_strace.rb`
|
|
123
|
+
- `examples/syscall_counter.rb`
|
|
124
|
+
- `examples/file_access_tracer.rb`
|
|
125
|
+
- `examples/memory_reader.rb`
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
- Generate docs: `bundle exec yard doc`
|
|
130
|
+
- Open index: `doc/index.html`
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bundle exec rspec
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Run specs with coverage threshold check:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
COVERAGE=1 COVERAGE_MIN_LINE=95 bundle exec rspec spec/unit spec/rptrace_spec.rb
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Generate syscall tables from Linux headers (`x86_64` / `aarch64`):
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
bundle exec rake syscall:generate
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
You can override header paths with:
|
|
151
|
+
|
|
152
|
+
- `PTRACE_SYSCALL_HEADER_X86_64`
|
|
153
|
+
- `PTRACE_SYSCALL_HEADER_AARCH64`
|
|
154
|
+
|
|
155
|
+
Optional task controls:
|
|
156
|
+
|
|
157
|
+
- `ARCH=x86_64` (or `ARCH=x86_64,aarch64`) to limit architectures
|
|
158
|
+
- `STRICT=1` to fail if any requested architecture header is missing
|
|
159
|
+
|
|
160
|
+
Generate YARD documentation:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
bundle exec yard doc
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Release
|
|
167
|
+
|
|
168
|
+
- CI release workflow: `.github/workflows/release.yml`
|
|
169
|
+
- Trigger by pushing a tag (example: `v0.1.0`) or via `workflow_dispatch`
|
|
170
|
+
- Set repository secret `RUBYGEMS_API_KEY` to enable `gem push`
|
|
171
|
+
- Local preflight: `bundle exec rake release:preflight`
|
|
172
|
+
- Local credential check: `bundle exec rake release:check_credentials`
|
|
173
|
+
|
|
174
|
+
## Limitations
|
|
175
|
+
|
|
176
|
+
- Linux only
|
|
177
|
+
- Ruby 3.1+
|
|
178
|
+
- Architecture support: `x86_64` and `aarch64`
|
|
179
|
+
- Integration tests require ptrace permission (`root` or `CAP_SYS_PTRACE`)
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require_relative "lib/rptrace/syscall_table/generator"
|
|
6
|
+
require "yaml"
|
|
7
|
+
|
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
9
|
+
|
|
10
|
+
namespace :syscall do
|
|
11
|
+
desc "Generate syscall tables from Linux headers"
|
|
12
|
+
task :generate do
|
|
13
|
+
arches = ENV.fetch("ARCH", "").split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
|
|
14
|
+
arches = Rptrace::SyscallTable::Generator::ARCH_CONFIG.keys if arches.empty?
|
|
15
|
+
strict = ENV["STRICT"] == "1"
|
|
16
|
+
|
|
17
|
+
if strict
|
|
18
|
+
results = Rptrace::SyscallTable::Generator.generate_all(root_dir: __dir__, arches: arches, skip_missing: false)
|
|
19
|
+
skipped = []
|
|
20
|
+
else
|
|
21
|
+
output = Rptrace::SyscallTable::Generator.generate_available(root_dir: __dir__, arches: arches)
|
|
22
|
+
results = output.fetch(:generated)
|
|
23
|
+
skipped = output.fetch(:skipped)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
abort("syscall:generate failed: no headers found for requested architectures") if results.empty?
|
|
27
|
+
|
|
28
|
+
results.each do |result|
|
|
29
|
+
puts "generated #{result[:arch]} table (#{result[:entries_count]} entries)"
|
|
30
|
+
puts " header: #{result[:header_path]}"
|
|
31
|
+
puts " output: #{result[:output_path]}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
skipped.each do |entry|
|
|
35
|
+
warn "skipped #{entry[:arch]}: #{entry[:reason]}"
|
|
36
|
+
end
|
|
37
|
+
rescue Rptrace::SyscallTable::Generator::HeaderNotFoundError, ArgumentError => e
|
|
38
|
+
abort("syscall:generate failed: #{e.message}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
namespace :release do
|
|
43
|
+
desc "Run release preflight checks (unit specs, docs, gem build)"
|
|
44
|
+
task :preflight do
|
|
45
|
+
sh "bundle exec rspec spec/unit spec/rptrace_spec.rb"
|
|
46
|
+
sh "bundle exec yard doc -n --no-cache"
|
|
47
|
+
sh "gem build rptrace.gemspec"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
desc "Check RubyGems API key availability for gem push"
|
|
51
|
+
task :check_credentials do
|
|
52
|
+
env_key = ENV["RUBYGEMS_API_KEY"]
|
|
53
|
+
if env_key && !env_key.strip.empty?
|
|
54
|
+
puts "RUBYGEMS_API_KEY is set in environment"
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
credentials_path = File.expand_path("~/.gem/credentials")
|
|
59
|
+
if File.readable?(credentials_path)
|
|
60
|
+
credentials = YAML.load_file(credentials_path) || {}
|
|
61
|
+
api_key = credentials[":rubygems_api_key"] || credentials["rubygems_api_key"]
|
|
62
|
+
if api_key && !api_key.to_s.strip.empty?
|
|
63
|
+
puts "RubyGems API key found in #{credentials_path}"
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
abort("release:check_credentials failed: RUBYGEMS_API_KEY is not configured")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
task default: :spec
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rptrace"
|
|
4
|
+
|
|
5
|
+
if ARGV.empty?
|
|
6
|
+
warn "usage: bundle exec ruby examples/file_access_tracer.rb <command> [args...]"
|
|
7
|
+
exit 1
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
command = ARGV.shift
|
|
11
|
+
|
|
12
|
+
Rptrace.strace(command, *ARGV) do |event|
|
|
13
|
+
next unless event.exit?
|
|
14
|
+
next unless %i[open openat].include?(event.syscall.name)
|
|
15
|
+
|
|
16
|
+
puts event.to_s
|
|
17
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rptrace"
|
|
4
|
+
|
|
5
|
+
pid_str = ARGV.shift || begin
|
|
6
|
+
warn "usage: bundle exec ruby examples/memory_reader.rb <pid> [address_hex] [length]"
|
|
7
|
+
exit 1
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
pid = Integer(pid_str, 10)
|
|
11
|
+
|
|
12
|
+
requested_address = nil
|
|
13
|
+
length_arg = nil
|
|
14
|
+
|
|
15
|
+
case ARGV.length
|
|
16
|
+
when 0
|
|
17
|
+
length_arg = "64"
|
|
18
|
+
when 1
|
|
19
|
+
token = ARGV[0]
|
|
20
|
+
if token.start_with?("0x", "0X")
|
|
21
|
+
requested_address = Integer(token, 0)
|
|
22
|
+
length_arg = "64"
|
|
23
|
+
else
|
|
24
|
+
length_arg = token
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
requested_address = Integer(ARGV[0], 0)
|
|
28
|
+
length_arg = ARGV[1]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
length = Integer(length_arg, 10)
|
|
32
|
+
raise ArgumentError, "length must be positive" if length <= 0
|
|
33
|
+
|
|
34
|
+
tracee = Rptrace::Tracee.attach(pid)
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
candidates = []
|
|
38
|
+
if requested_address
|
|
39
|
+
candidates << requested_address
|
|
40
|
+
else
|
|
41
|
+
registers = tracee.registers.read
|
|
42
|
+
arch_candidates = case Rptrace::CStructs.arch
|
|
43
|
+
when :x86_64 then [registers[:rdi], registers[:rsp]]
|
|
44
|
+
when :aarch64 then [registers[:x0], registers[:sp]]
|
|
45
|
+
else []
|
|
46
|
+
end
|
|
47
|
+
candidates.concat(arch_candidates.compact)
|
|
48
|
+
tracee.memory_maps.each do |map|
|
|
49
|
+
next unless map.readable?
|
|
50
|
+
|
|
51
|
+
candidates << map.start_addr
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
address = candidates.find do |candidate|
|
|
56
|
+
tracee.memory.read(candidate, 1)
|
|
57
|
+
true
|
|
58
|
+
rescue Rptrace::Error
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
unless address
|
|
63
|
+
warn "failed to find readable address for pid=#{pid}"
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
data = tracee.memory.read(address, length)
|
|
68
|
+
puts "pid=#{pid} addr=0x#{address.to_s(16)} bytes=#{data.unpack1('H*')} size=#{data.bytesize}"
|
|
69
|
+
ensure
|
|
70
|
+
tracee&.detach
|
|
71
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rptrace"
|
|
4
|
+
|
|
5
|
+
if ARGV.empty?
|
|
6
|
+
warn "usage: bundle exec ruby examples/simple_strace.rb <command> [args...]"
|
|
7
|
+
exit 1
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
command = ARGV.shift
|
|
11
|
+
|
|
12
|
+
Rptrace.strace(command, *ARGV) do |event|
|
|
13
|
+
next unless event.exit?
|
|
14
|
+
|
|
15
|
+
puts event
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rptrace"
|
|
4
|
+
|
|
5
|
+
if ARGV.empty?
|
|
6
|
+
warn "usage: bundle exec ruby examples/syscall_counter.rb <command> [args...]"
|
|
7
|
+
exit 1
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
command = ARGV.shift
|
|
11
|
+
counts = Hash.new(0)
|
|
12
|
+
|
|
13
|
+
Rptrace.strace(command, *ARGV) do |event|
|
|
14
|
+
next unless event.exit?
|
|
15
|
+
|
|
16
|
+
counts[event.syscall.name] += 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
counts.sort_by { |(_name, count)| -count }.each do |name, count|
|
|
20
|
+
puts format("%-20s %d", name, count)
|
|
21
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
require "fiddle/import"
|
|
5
|
+
|
|
6
|
+
module Rptrace
|
|
7
|
+
# Low-level libc bindings for ptrace and waitpid.
|
|
8
|
+
module Binding
|
|
9
|
+
extend Fiddle::Importer
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
dlload Fiddle::Handle::DEFAULT
|
|
13
|
+
|
|
14
|
+
extern "long ptrace(int, int, unsigned long, unsigned long)"
|
|
15
|
+
extern "int waitpid(int, void*, int)"
|
|
16
|
+
extern "int fork()"
|
|
17
|
+
extern "int execvp(char*, void*)"
|
|
18
|
+
|
|
19
|
+
# Maps errno values to specific Rptrace error subclasses.
|
|
20
|
+
ERRNO_CLASS_MAP = {
|
|
21
|
+
Errno::EPERM::Errno => PermissionError,
|
|
22
|
+
Errno::ESRCH::Errno => NoProcessError,
|
|
23
|
+
Errno::EBUSY::Errno => BusyError,
|
|
24
|
+
Errno::EINVAL::Errno => InvalidArgError
|
|
25
|
+
}.freeze
|
|
26
|
+
# Guidance suffix appended to EPERM-related ptrace errors.
|
|
27
|
+
PERMISSION_HINT = "try running as root, granting CAP_SYS_PTRACE, and checking /proc/sys/kernel/yama/ptrace_scope".freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Calls ptrace and raises mapped Rptrace::Error subclasses on failure.
|
|
31
|
+
#
|
|
32
|
+
# @param request [Integer, Symbol]
|
|
33
|
+
# @param pid [Integer]
|
|
34
|
+
# @param addr [Integer]
|
|
35
|
+
# @param data [Integer]
|
|
36
|
+
# @return [Integer]
|
|
37
|
+
def safe_ptrace(request, pid, addr, data)
|
|
38
|
+
clear_errno!
|
|
39
|
+
result = ptrace(request, pid, addr, data)
|
|
40
|
+
errno = Fiddle.last_error
|
|
41
|
+
return result unless result == -1 && errno.positive?
|
|
42
|
+
|
|
43
|
+
raise_ptrace_error(errno, request)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Calls waitpid and decodes raw status.
|
|
47
|
+
#
|
|
48
|
+
# @param pid [Integer]
|
|
49
|
+
# @param flags [Integer]
|
|
50
|
+
# @return [Array<(Integer, Integer)>] waited pid and raw status
|
|
51
|
+
def safe_waitpid(pid, flags: 0)
|
|
52
|
+
status_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
|
|
53
|
+
|
|
54
|
+
loop do
|
|
55
|
+
clear_errno!
|
|
56
|
+
waited_pid = waitpid(pid, status_ptr, flags)
|
|
57
|
+
errno = Fiddle.last_error
|
|
58
|
+
|
|
59
|
+
next if waited_pid == -1 && errno == Errno::EINTR::Errno
|
|
60
|
+
raise_ptrace_error(errno, :waitpid) if waited_pid == -1
|
|
61
|
+
|
|
62
|
+
status = status_ptr[0, Fiddle::SIZEOF_INT].unpack1("i")
|
|
63
|
+
return [waited_pid, status]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Resets thread-local errno to zero.
|
|
68
|
+
#
|
|
69
|
+
# @return [Integer]
|
|
70
|
+
def clear_errno!
|
|
71
|
+
Fiddle.last_error = 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Raises a mapped Rptrace::Error subclass for an errno code.
|
|
75
|
+
#
|
|
76
|
+
# @param errno [Integer]
|
|
77
|
+
# @param request [Integer, Symbol]
|
|
78
|
+
# @raise [Rptrace::Error]
|
|
79
|
+
# @return [void]
|
|
80
|
+
def raise_ptrace_error(errno, request)
|
|
81
|
+
klass = ERRNO_CLASS_MAP.fetch(errno, Error)
|
|
82
|
+
message = SystemCallError.new("ptrace", errno).message
|
|
83
|
+
message = "#{message}; #{PERMISSION_HINT}" if klass == PermissionError
|
|
84
|
+
raise klass.new(message, errno: errno, request: request)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rptrace
|
|
4
|
+
# Software breakpoint descriptor for a traced process.
|
|
5
|
+
class Breakpoint
|
|
6
|
+
attr_reader :tracee, :address, :original_byte
|
|
7
|
+
|
|
8
|
+
# @param tracee [Rptrace::Tracee]
|
|
9
|
+
# @param address [Integer]
|
|
10
|
+
# @param original_byte [String] one-byte opcode before patching
|
|
11
|
+
# @param enabled [Boolean]
|
|
12
|
+
def initialize(tracee:, address:, original_byte:, enabled: true)
|
|
13
|
+
byte = original_byte.to_s.b
|
|
14
|
+
raise ArgumentError, "original_byte must be exactly one byte" unless byte.bytesize == 1
|
|
15
|
+
|
|
16
|
+
@tracee = tracee
|
|
17
|
+
@address = Integer(address)
|
|
18
|
+
@original_byte = byte
|
|
19
|
+
@enabled = enabled
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def enabled?
|
|
24
|
+
@enabled
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Rptrace::Breakpoint]
|
|
28
|
+
def disable!
|
|
29
|
+
@enabled = false
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Restores original opcode through the owning tracee.
|
|
34
|
+
#
|
|
35
|
+
# @return [Rptrace::Breakpoint, nil]
|
|
36
|
+
def restore
|
|
37
|
+
tracee.remove_breakpoint(address)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String]
|
|
41
|
+
def inspect
|
|
42
|
+
state = enabled? ? "enabled" : "disabled"
|
|
43
|
+
"#<#{self.class} addr=0x#{address.to_s(16)} state=#{state}>"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|