seccomp-notify 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +132 -43
- data/lib/seccomp/notify/bpf/builder.rb +54 -4
- data/lib/seccomp/notify/constants.rb +4 -0
- data/lib/seccomp/notify/fd_passing.rb +1 -1
- data/lib/seccomp/notify/features.rb +50 -36
- data/lib/seccomp/notify/filter.rb +2 -2
- data/lib/seccomp/notify/libc.rb +24 -7
- data/lib/seccomp/notify/policy.rb +19 -1
- data/lib/seccomp/notify/request.rb +12 -2
- data/lib/seccomp/notify/supervisor.rb +45 -17
- data/lib/seccomp/notify/target_memory.rb +19 -13
- data/lib/seccomp/notify/version.rb +1 -1
- data/lib/seccomp/notify.rb +17 -10
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1338763ff6ac458c1224576b7763524b57034de1b29423acc2dde48268012dd1
|
|
4
|
+
data.tar.gz: 03dc39b44de12c7bb1401edc303097eb02b389c511e040589c2293cb840d2c41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c99782a7d1c9bf43d3069aa1f742f1c3f5e33b40a47014c6e5a3d4803062b8d8239e2df8a915583d056a4441ed438deebde1ad1cc8af9e1a1bdd8b5f95e333f
|
|
7
|
+
data.tar.gz: dd0f9b82f5749bdeb933e6e56c616619f150fadbb9742582f03ef32ff8d8401e7732dff4070b7a0860abcd6bb42ed537047cda119fa314891c99ab51af0bbd4e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to seccomp-notify are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and versions follow [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## v0.3.0 - 2026-08-24
|
|
9
|
+
|
|
10
|
+
- Kill the issuing thread group without treating a thread ID as a process ID.
|
|
11
|
+
- Keep the listener transfer descriptor reserved so later `sendmsg` calls cannot bypass policy.
|
|
12
|
+
- Reject x32 syscalls, conflicting policy actions, and invalid emulated return values.
|
|
13
|
+
- Preserve the protocol-neutral type of decoded socket addresses.
|
|
14
|
+
- Marshal ioctl and seccomp buffers through native memory for compacting-GC safety.
|
|
15
|
+
- Reduce runtime feature detection from four isolated child probes to two and synchronize supervisor shutdown state.
|
|
16
|
+
|
|
17
|
+
## v0.2.0 - 2026-08-22
|
|
18
|
+
|
|
19
|
+
- Stop supervising a non-child target once it becomes a zombie.
|
|
20
|
+
- Read target memory from the current address space after `exec` and normalize EOF failures.
|
|
21
|
+
|
|
8
22
|
## v0.1.0 - 2026-08-22
|
|
9
23
|
|
|
10
24
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,82 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<h1 align="center">seccomp-notify</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Pure Ruby bindings for Linux seccomp user notifications</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/seccomp-notify"><img src="https://img.shields.io/gem/v/seccomp-notify.svg?colorB=319e8c" alt="Gem Version"></a>
|
|
9
|
+
<a href="https://rubygems.org/gems/seccomp-notify"><img src="https://img.shields.io/gem/dt/seccomp-notify.svg" alt="Downloads"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.1-ruby.svg" alt="Ruby Version">
|
|
11
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="#features">Features</a> ·
|
|
16
|
+
<a href="#installation">Installation</a> ·
|
|
17
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
18
|
+
<a href="https://ydah.github.io/seccomp-notify/">API Docs</a> ·
|
|
19
|
+
<a href="#responses">Responses</a> ·
|
|
20
|
+
<a href="#security">Security</a> ·
|
|
21
|
+
<a href="#development">Development</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
`seccomp-notify` provides the plumbing for Linux
|
|
27
|
+
[`SECCOMP_RET_USER_NOTIF`](https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html).
|
|
28
|
+
It builds cBPF filters, transfers listener file descriptors, supervises syscall notifications,
|
|
29
|
+
reads target memory, and injects file descriptors without a native extension.
|
|
4
30
|
|
|
5
31
|
> [!WARNING]
|
|
6
|
-
> This gem is a mechanism, not a security policy or a complete sandbox. Never inspect a pointer
|
|
32
|
+
> This gem is a mechanism, not a security policy or a complete sandbox. Never inspect a pointer
|
|
33
|
+
> argument and then use `continue!` as a security decision: another target thread can change that
|
|
34
|
+
> memory between inspection and syscall execution (TOCTOU). Reject or emulate the syscall instead.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- Pure Ruby cBPF filter generation and seccomp installation
|
|
39
|
+
- Parent supervisor with a filtered child through `Seccomp::Notify.spawn`
|
|
40
|
+
- Existing-process supervision through `Seccomp::Notify.supervise_self`
|
|
41
|
+
- Syscall handlers with errno, emulation, continuation, and process termination responses
|
|
42
|
+
- Target memory helpers for strings, bytes, and socket addresses
|
|
43
|
+
- File descriptor injection with atomic and fallback ADDFD paths
|
|
44
|
+
- Runtime detection for optional kernel features
|
|
45
|
+
- Handler timeouts, concurrent workers, and supervisor health checks
|
|
46
|
+
- Linux x86_64 and aarch64 support
|
|
7
47
|
|
|
8
|
-
|
|
48
|
+
## Installation
|
|
9
49
|
|
|
10
|
-
|
|
50
|
+
Add the gem to your Gemfile:
|
|
11
51
|
|
|
12
|
-
|
|
52
|
+
```ruby
|
|
53
|
+
gem "seccomp-notify"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then install:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
bundle install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Requirements
|
|
13
63
|
|
|
14
64
|
- Linux on x86_64 or aarch64
|
|
15
65
|
- Ruby 3.1 or newer
|
|
16
|
-
- Linux 5.0 or newer; ADDFD
|
|
66
|
+
- Linux 5.0 or newer; ADDFD requires Linux 5.9 or newer
|
|
17
67
|
- `CONFIG_SECCOMP_FILTER=y`
|
|
18
68
|
|
|
19
|
-
Container runtimes may block `seccomp(2)` with their own profile. For Docker development, use an
|
|
69
|
+
Container runtimes may block `seccomp(2)` with their own profile. For Docker development, use an
|
|
70
|
+
isolated test container with seccomp disabled:
|
|
20
71
|
|
|
21
72
|
```sh
|
|
22
73
|
docker run --rm --security-opt seccomp=unconfined -v "$PWD:/app" -w /app ruby:3.4 bundle exec rake
|
|
23
74
|
```
|
|
24
75
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
```ruby
|
|
28
|
-
gem "seccomp-notify"
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Usage
|
|
76
|
+
## Quick Start
|
|
32
77
|
|
|
33
|
-
|
|
78
|
+
Keep the unfiltered supervisor in the parent and install the filter in a child:
|
|
34
79
|
|
|
35
80
|
```ruby
|
|
36
81
|
require "seccomp/notify"
|
|
37
82
|
|
|
38
|
-
open_syscalls = RUBY_PLATFORM.include?("x86_64") ? %i[open openat] : %i[openat]
|
|
39
83
|
policy = Seccomp::Notify::Policy.new do
|
|
40
|
-
notify
|
|
84
|
+
notify :connect, :sendto
|
|
41
85
|
end
|
|
42
86
|
|
|
43
87
|
supervisor = Seccomp::Notify.spawn(policy) do
|
|
44
|
-
exec("
|
|
88
|
+
exec("curl", "https://example.com")
|
|
45
89
|
end
|
|
46
90
|
|
|
47
|
-
open_handler = proc do |request|
|
|
48
|
-
path_argument = request.syscall == :open ? 0 : 1
|
|
49
|
-
path = request.read_string(request.args[path_argument])
|
|
50
|
-
if path == "/etc/shadow"
|
|
51
|
-
request.error!(Errno::EACCES)
|
|
52
|
-
else
|
|
53
|
-
request.continue!(unsafe: true)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
open_syscalls.each { |syscall| supervisor.on(syscall, &open_handler) }
|
|
57
|
-
|
|
58
91
|
supervisor.on(:connect) { |request| request.error!(Errno::ENETUNREACH) }
|
|
92
|
+
supervisor.on(:sendto) { |request| request.error!(Errno::ENETUNREACH) }
|
|
93
|
+
|
|
59
94
|
status = supervisor.run
|
|
60
95
|
```
|
|
61
96
|
|
|
62
|
-
`request.pid` is an alias for `request.tid`: the kernel reports the thread ID that issued the
|
|
97
|
+
`request.pid` is an alias for `request.tid`: the kernel reports the thread ID that issued the
|
|
98
|
+
syscall, not the process ID.
|
|
99
|
+
|
|
100
|
+
### Supervise the current process
|
|
101
|
+
|
|
102
|
+
`supervise_self(policy, supervisor: :fork)` accepts a block that configures the child supervisor.
|
|
103
|
+
The safer `:spawn` form starts a clean Ruby VM and supports the default pass-through handler only.
|
|
104
|
+
Both forms detach the supervisor child, so broad `Process.wait` calls in the target can still
|
|
105
|
+
observe related lifecycle effects. The default pass-through handler requires `continue!` and Linux
|
|
106
|
+
5.5 or newer; handler-based supervision works on Linux 5.0 or newer.
|
|
107
|
+
|
|
108
|
+
## Responses
|
|
109
|
+
|
|
110
|
+
| Method | Behavior | Requirement |
|
|
111
|
+
|--------|----------|-------------|
|
|
112
|
+
| `allow!(value = 0)` | Emulate a successful return value without running the syscall | Linux 5.0+ |
|
|
113
|
+
| `error!(Errno::EPERM)` | Return an errno | Linux 5.0+ |
|
|
114
|
+
| `continue!` | Run the original syscall | Linux 5.5+ |
|
|
115
|
+
| `add_fd!(io)` | Inject a file descriptor and return its number | Linux 5.9+ |
|
|
116
|
+
| `kill!` | Terminate the issuing process | — |
|
|
117
|
+
|
|
118
|
+
Use `Seccomp::Notify.features` to inspect runtime support. Set
|
|
119
|
+
`SECCOMP_NOTIFY_DISABLE_FEATURES=addfd,continue` to force feature fallbacks in tests.
|
|
120
|
+
|
|
121
|
+
Targets started by this gem can call `Seccomp::Notify.supervisor_alive?` to poll the health pipe.
|
|
122
|
+
If the target calls `exec`, the pipe descriptor is inherited and published as
|
|
123
|
+
`SECCOMP_NOTIFY_HEALTH_FD`.
|
|
63
124
|
|
|
64
|
-
|
|
125
|
+
## How It Works
|
|
65
126
|
|
|
66
|
-
|
|
127
|
+
1. The parent creates a Unix socket pair and forks the target.
|
|
128
|
+
2. The target installs a cBPF filter and sends its listener descriptor to the parent with
|
|
129
|
+
`SCM_RIGHTS`.
|
|
130
|
+
3. The parent receives syscall notifications and dispatches them to registered handlers.
|
|
131
|
+
4. Each handler returns an errno, emulated value, continuation, or injected descriptor.
|
|
67
132
|
|
|
68
|
-
|
|
69
|
-
- `error!(Errno::EPERM)` returns an errno.
|
|
70
|
-
- `continue!` executes the original syscall when supported.
|
|
71
|
-
- `add_fd!(io)` injects a file descriptor when supported.
|
|
72
|
-
- `kill!` terminates the issuing task.
|
|
133
|
+
## Security
|
|
73
134
|
|
|
74
|
-
|
|
135
|
+
- Pointer data may change after inspection. Never use it to justify `continue!`.
|
|
136
|
+
- vDSO calls, existing file descriptors, and indirect shared-memory effects are outside seccomp's
|
|
137
|
+
control.
|
|
138
|
+
- I/O submitted through an existing `io_uring` does not pass through the filter;
|
|
139
|
+
`io_uring_setup` is denied by default.
|
|
140
|
+
- If the supervisor dies, blocked notification syscalls return `ENOSYS`.
|
|
141
|
+
- `fcntl` must remain allowed while Ruby wraps the listener descriptor, so policies that notify or
|
|
142
|
+
deny it are rejected. `sendmsg` may be filtered; only listener transfer on its reserved descriptor
|
|
143
|
+
is allowed, and that descriptor remains open across `exec` to prevent fd-number reuse.
|
|
75
144
|
|
|
76
|
-
|
|
145
|
+
## Examples
|
|
146
|
+
|
|
147
|
+
- [`examples/deny_network.rb`](examples/deny_network.rb) rejects outbound network syscalls.
|
|
148
|
+
- [`examples/emulate_open.rb`](examples/emulate_open.rb) replaces `/dev/urandom` with an injected
|
|
149
|
+
file descriptor.
|
|
150
|
+
- [`examples/strace_lite.rb`](examples/strace_lite.rb) logs selected syscall notifications.
|
|
77
151
|
|
|
78
152
|
## Development
|
|
79
153
|
|
|
80
|
-
|
|
154
|
+
```sh
|
|
155
|
+
bundle install
|
|
156
|
+
bundle exec rake
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Linux runs include real seccomp integration tests; other systems run portable filter and layout
|
|
160
|
+
tests only. `spike/notify_min.c` is the reference C round trip, and
|
|
161
|
+
`tools/gen_syscall_table.rb` regenerates architecture tables from Linux kernel syscall tables.
|
|
162
|
+
|
|
163
|
+
## Contributing
|
|
164
|
+
|
|
165
|
+
Bug reports and pull requests are welcome at https://github.com/ydah/seccomp-notify.
|
|
166
|
+
|
|
167
|
+
Releases before 1.0 may change the API.
|
|
168
|
+
|
|
169
|
+
## License
|
|
81
170
|
|
|
82
|
-
|
|
171
|
+
Released under the [MIT License](LICENSE.txt).
|
|
@@ -4,31 +4,45 @@ module Seccomp
|
|
|
4
4
|
module Notify
|
|
5
5
|
module BPF
|
|
6
6
|
class Builder
|
|
7
|
-
LINEAR_LIMIT =
|
|
7
|
+
LINEAR_LIMIT = 4
|
|
8
8
|
AUDIT_ARCHES = {x86_64: Constants::AUDIT_ARCH_X86_64, aarch64: Constants::AUDIT_ARCH_AARCH64}.freeze
|
|
9
9
|
|
|
10
|
-
def initialize(policy, arch: Libc.architecture)
|
|
10
|
+
def initialize(policy, arch: Libc.architecture, transfer_fd: nil)
|
|
11
11
|
raise NotSupportedError, "unsupported architecture: #{arch}" unless AUDIT_ARCHES.key?(arch)
|
|
12
|
+
raise ArgumentError, "transfer_fd must be a non-negative integer" if transfer_fd && (!transfer_fd.is_a?(Integer) || transfer_fd.negative?)
|
|
12
13
|
|
|
13
14
|
@policy = policy
|
|
14
15
|
@arch = arch
|
|
16
|
+
@transfer_fd = transfer_fd
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def build
|
|
18
20
|
decisions = @policy.decisions(@arch)
|
|
21
|
+
if @transfer_fd.nil? && @policy.default_action != Constants::SECCOMP_RET_ALLOW && !@policy.notified?(:sendmsg)
|
|
22
|
+
decisions[Syscalls.number(:sendmsg, @arch)] = Constants::SECCOMP_RET_ALLOW
|
|
23
|
+
end
|
|
19
24
|
body = decisions.length <= LINEAR_LIMIT ? linear(decisions) : tree(decisions.sort)
|
|
20
|
-
|
|
25
|
+
conditions = @policy.conditional_notifications(@arch)
|
|
26
|
+
bootstrap_first = @transfer_fd && conditions.any? { |number, _argument, _mask| number == Syscalls.number(:sendmsg, @arch) }
|
|
27
|
+
conditional = conditional_notifications(conditions, decisions)
|
|
28
|
+
bootstrap = bootstrap_first ? transfer_allowance + conditional : conditional + transfer_allowance
|
|
29
|
+
Program.new(prologue + bootstrap + body)
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
private
|
|
24
33
|
|
|
25
34
|
def prologue
|
|
26
|
-
[
|
|
35
|
+
instructions = [
|
|
27
36
|
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_ARCH),
|
|
28
37
|
ins(Constants::BPF_JMP_JEQ_K, 1, 0, AUDIT_ARCHES.fetch(@arch)),
|
|
29
38
|
ins(Constants::BPF_RET_K, 0, 0, Constants::SECCOMP_RET_KILL_PROCESS),
|
|
30
39
|
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_NR)
|
|
31
40
|
]
|
|
41
|
+
if @arch == :x86_64
|
|
42
|
+
instructions << ins(Constants::BPF_JMP_JGE_K, 0, 1, Constants::X32_SYSCALL_BIT)
|
|
43
|
+
instructions << ins(Constants::BPF_RET_K, 0, 0, Constants::SECCOMP_RET_KILL_PROCESS)
|
|
44
|
+
end
|
|
45
|
+
instructions
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
def linear(decisions)
|
|
@@ -37,6 +51,42 @@ module Seccomp
|
|
|
37
51
|
end << ins(Constants::BPF_RET_K, 0, 0, @policy.default_action)
|
|
38
52
|
end
|
|
39
53
|
|
|
54
|
+
def transfer_allowance
|
|
55
|
+
return [] unless @transfer_fd
|
|
56
|
+
|
|
57
|
+
conditional(Syscalls.number(:sendmsg, @arch), 0, @transfer_fd, Constants::BPF_JMP_JEQ_K)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def conditional_notifications(conditions, decisions)
|
|
61
|
+
last_condition = {}
|
|
62
|
+
conditions.each_with_index { |(number, _argument, _mask), index| last_condition[number] = index }
|
|
63
|
+
conditions.each_with_index.flat_map do |(number, argument, mask), index|
|
|
64
|
+
fallback = decisions.fetch(number, @policy.default_action) if last_condition.fetch(number) == index
|
|
65
|
+
conditional(number, argument, mask, Constants::BPF_JMP_JSET_K, fallback:)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def conditional(number, argument, value, comparison, fallback: nil)
|
|
70
|
+
if fallback
|
|
71
|
+
return [
|
|
72
|
+
ins(Constants::BPF_JMP_JEQ_K, 0, 4, number),
|
|
73
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_ARGS + argument * 8),
|
|
74
|
+
ins(comparison, 0, 1, value),
|
|
75
|
+
ins(Constants::BPF_RET_K, 0, 0, Constants::SECCOMP_RET_USER_NOTIF),
|
|
76
|
+
ins(Constants::BPF_RET_K, 0, 0, fallback),
|
|
77
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_NR)
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[
|
|
82
|
+
ins(Constants::BPF_JMP_JEQ_K, 0, 3, number),
|
|
83
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_ARGS + argument * 8),
|
|
84
|
+
ins(comparison, 0, 1, value),
|
|
85
|
+
ins(Constants::BPF_RET_K, 0, 0, comparison == Constants::BPF_JMP_JEQ_K ? Constants::SECCOMP_RET_ALLOW : Constants::SECCOMP_RET_USER_NOTIF),
|
|
86
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_NR)
|
|
87
|
+
]
|
|
88
|
+
end
|
|
89
|
+
|
|
40
90
|
def tree(decisions)
|
|
41
91
|
return [ins(Constants::BPF_RET_K, 0, 0, @policy.default_action)] if decisions.empty?
|
|
42
92
|
return linear(decisions) if decisions.length <= 4
|
|
@@ -32,13 +32,17 @@ module Seccomp
|
|
|
32
32
|
AUDIT_ARCH_X86_64 = 0xc000_003e
|
|
33
33
|
AUDIT_ARCH_AARCH64 = 0xc000_00b7
|
|
34
34
|
AUDIT_ARCH_I386 = 0x4000_0003
|
|
35
|
+
X32_SYSCALL_BIT = 0x4000_0000
|
|
35
36
|
SYS_SECCOMP = {x86_64: 317, aarch64: 277}.freeze
|
|
37
|
+
SYS_TGKILL = {x86_64: 234, aarch64: 131}.freeze
|
|
36
38
|
|
|
37
39
|
PR_SET_NO_NEW_PRIVS = 38
|
|
40
|
+
PR_SET_PTRACER = 0x5961_6d61
|
|
38
41
|
BPF_LD_W_ABS = 0x20
|
|
39
42
|
BPF_JMP_JA = 0x05
|
|
40
43
|
BPF_JMP_JEQ_K = 0x15
|
|
41
44
|
BPF_JMP_JGE_K = 0x35
|
|
45
|
+
BPF_JMP_JSET_K = 0x45
|
|
42
46
|
BPF_RET_K = 0x06
|
|
43
47
|
BPF_MAXINSNS = 4096
|
|
44
48
|
MAX_ERRNO = 4095
|
|
@@ -12,9 +12,7 @@ module Seccomp
|
|
|
12
12
|
result = {user_notif: supported?, continue: false, addfd: false, addfd_send: false, wait_killable_recv: false}
|
|
13
13
|
return result.freeze unless result[:user_notif]
|
|
14
14
|
|
|
15
|
-
result
|
|
16
|
-
result[:addfd] = probe_addfd(0)
|
|
17
|
-
result[:addfd_send] = probe_addfd(Constants::SECCOMP_ADDFD_FLAG_SEND)
|
|
15
|
+
result.update(probe_responses)
|
|
18
16
|
result[:wait_killable_recv] = probe_install(Constants::SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV)
|
|
19
17
|
disabled.each { |name| result[name.to_sym] = false if result.key?(name.to_sym) }
|
|
20
18
|
result[:addfd_send] = false unless result[:addfd]
|
|
@@ -28,21 +26,36 @@ module Seccomp
|
|
|
28
26
|
false
|
|
29
27
|
end
|
|
30
28
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def probe_addfd(flags)
|
|
29
|
+
def probe_responses
|
|
30
|
+
parent, child = UNIXSocket.pair
|
|
31
|
+
listener = nil
|
|
32
|
+
pid = nil
|
|
39
33
|
File.open("/dev/null") do |source|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
pid = fork do
|
|
35
|
+
parent.close
|
|
36
|
+
listener = Filter.install!(BPF::Builder.new(Policy.new { notify :getpid }).build)
|
|
37
|
+
FdPassing.send_fd(child, listener)
|
|
38
|
+
3.times { Libc::SYSCALL.call(Syscalls.number(:getpid), 0, 0, 0) }
|
|
39
|
+
exit! 0
|
|
40
|
+
rescue StandardError
|
|
41
|
+
exit! 1
|
|
44
42
|
end
|
|
43
|
+
child.close
|
|
44
|
+
listener = FdPassing.recv_fd(parent, timeout: 2)
|
|
45
|
+
result = {
|
|
46
|
+
continue: probe_response(listener, Constants::SECCOMP_USER_NOTIF_FLAG_CONTINUE),
|
|
47
|
+
addfd: probe_addfd(listener, source, 0),
|
|
48
|
+
addfd_send: probe_addfd(listener, source, Constants::SECCOMP_ADDFD_FLAG_SEND)
|
|
49
|
+
}
|
|
50
|
+
return result if wait_for_probe(pid).success?
|
|
45
51
|
end
|
|
52
|
+
{continue: false, addfd: false, addfd_send: false}
|
|
53
|
+
rescue SystemCallError, EOFError, Timeout::Error
|
|
54
|
+
terminate_probe(pid)
|
|
55
|
+
{continue: false, addfd: false, addfd_send: false}
|
|
56
|
+
ensure
|
|
57
|
+
listener&.close unless listener&.closed?
|
|
58
|
+
parent&.close unless parent&.closed?
|
|
46
59
|
end
|
|
47
60
|
|
|
48
61
|
def probe_install(flags)
|
|
@@ -67,30 +80,31 @@ module Seccomp
|
|
|
67
80
|
parent&.close unless parent&.closed?
|
|
68
81
|
end
|
|
69
82
|
|
|
70
|
-
def
|
|
71
|
-
parent, child = UNIXSocket.pair
|
|
72
|
-
pid = fork do
|
|
73
|
-
parent.close
|
|
74
|
-
listener = Filter.install!(BPF::Builder.new(Policy.new { notify :getpid }).build)
|
|
75
|
-
FdPassing.send_fd(child, listener)
|
|
76
|
-
Libc::SYSCALL.call(Syscalls.number(:getpid), 0, 0, 0)
|
|
77
|
-
exit! 0
|
|
78
|
-
rescue StandardError
|
|
79
|
-
exit! 1
|
|
80
|
-
end
|
|
81
|
-
child.close
|
|
82
|
-
listener = FdPassing.recv_fd(parent, timeout: 2)
|
|
83
|
+
def probe_response(listener, flags)
|
|
83
84
|
request = "\0" * Structs::NOTIF_SIZE
|
|
84
85
|
Ioctl.call(listener, Ioctl::NOTIF_RECV, request)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
rescue SystemCallError
|
|
89
|
-
|
|
86
|
+
id = request.unpack1("Q<")
|
|
87
|
+
Ioctl.call(listener, Ioctl::NOTIF_SEND, [id, 0, 0, flags].pack(Structs::RESPONSE_FORMAT))
|
|
88
|
+
true
|
|
89
|
+
rescue SystemCallError
|
|
90
|
+
respond_to_probe(listener, id)
|
|
90
91
|
false
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def probe_addfd(listener, source, flags)
|
|
95
|
+
request = "\0" * Structs::NOTIF_SIZE
|
|
96
|
+
Ioctl.call(listener, Ioctl::NOTIF_RECV, request)
|
|
97
|
+
id = request.unpack1("Q<")
|
|
98
|
+
injected = Ioctl.call(listener, Ioctl::NOTIF_ADDFD, [id, flags, source.fileno, 0, 0].pack(Structs::ADDFD_FORMAT))
|
|
99
|
+
respond_to_probe(listener, id, injected) if flags.zero?
|
|
100
|
+
true
|
|
101
|
+
rescue SystemCallError
|
|
102
|
+
respond_to_probe(listener, id)
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def respond_to_probe(listener, id, value = 0)
|
|
107
|
+
Ioctl.call(listener, Ioctl::NOTIF_SEND, [id, value, 0, 0].pack(Structs::RESPONSE_FORMAT)) if id
|
|
94
108
|
end
|
|
95
109
|
|
|
96
110
|
def wait_for_probe(pid)
|
|
@@ -5,7 +5,7 @@ module Seccomp
|
|
|
5
5
|
module Filter
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
-
def install!(program, flags: 0)
|
|
8
|
+
def install!(program, flags: 0, raw: false)
|
|
9
9
|
unless Libc.prctl(Constants::PR_SET_NO_NEW_PRIVS, 1).zero?
|
|
10
10
|
raise SystemCallError.new("prctl(PR_SET_NO_NEW_PRIVS)", Fiddle.last_error)
|
|
11
11
|
end
|
|
@@ -14,7 +14,7 @@ module Seccomp
|
|
|
14
14
|
fd = Libc.seccomp(Constants::SECCOMP_SET_MODE_FILTER, filter_flags, program.to_sock_fprog)
|
|
15
15
|
raise SystemCallError.new("seccomp(SET_MODE_FILTER)", Fiddle.last_error) if fd.negative?
|
|
16
16
|
|
|
17
|
-
IO.for_fd(fd, "r")
|
|
17
|
+
raw ? fd : IO.for_fd(fd, "r")
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
data/lib/seccomp/notify/libc.rb
CHANGED
|
@@ -20,8 +20,7 @@ module Seccomp
|
|
|
20
20
|
IOCTL = Fiddle::Function.new(
|
|
21
21
|
HANDLE["ioctl"],
|
|
22
22
|
[Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP],
|
|
23
|
-
Fiddle::TYPE_INT
|
|
24
|
-
need_gvl: true
|
|
23
|
+
Fiddle::TYPE_INT
|
|
25
24
|
)
|
|
26
25
|
POLL = Fiddle::Function.new(
|
|
27
26
|
HANDLE["poll"],
|
|
@@ -47,20 +46,29 @@ module Seccomp
|
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
def seccomp(operation, flags, buffer = 0)
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
with_pointer(buffer) do |pointer|
|
|
50
|
+
SYSCALL.call(Constants::SYS_SECCOMP.fetch(architecture), operation, flags, pointer.to_i)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tgkill(tgid, tid, signal)
|
|
55
|
+
result = SYSCALL.call(Constants::SYS_TGKILL.fetch(architecture), tgid, tid, signal)
|
|
56
|
+
raise SystemCallError.new("tgkill", Fiddle.last_error) if result.negative?
|
|
57
|
+
|
|
58
|
+
result
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
def ioctl(fd, request, buffer)
|
|
55
|
-
IOCTL.call(fd, request,
|
|
62
|
+
with_pointer(buffer) { |pointer| IOCTL.call(fd, request, pointer) }
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def poll_hup?(fd)
|
|
59
66
|
buffer = [fd, 1, 0].pack("l<s<s<")
|
|
60
|
-
result = POLL.call(
|
|
67
|
+
result = with_pointer(buffer) { |pointer| POLL.call(pointer, 1, 0) }
|
|
61
68
|
raise SystemCallError.new("poll", Fiddle.last_error) if result.negative?
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
revents = buffer.unpack1("@6S<")
|
|
71
|
+
(revents & 0x10).positive? && (revents & 0x01).zero?
|
|
64
72
|
end
|
|
65
73
|
|
|
66
74
|
def notif_sizes
|
|
@@ -71,6 +79,15 @@ module Seccomp
|
|
|
71
79
|
notif, resp, data = buffer.unpack("S<S<S<")
|
|
72
80
|
{notif: notif, resp: resp, data: data}.freeze
|
|
73
81
|
end
|
|
82
|
+
|
|
83
|
+
def with_pointer(buffer)
|
|
84
|
+
return yield buffer unless buffer.is_a?(String)
|
|
85
|
+
|
|
86
|
+
pointer = Fiddle::Pointer.malloc(buffer.bytesize)
|
|
87
|
+
pointer[0, buffer.bytesize] = buffer
|
|
88
|
+
yield(pointer).tap { buffer.replace(pointer[0, buffer.bytesize]) }
|
|
89
|
+
end
|
|
90
|
+
private_class_method :with_pointer
|
|
74
91
|
end
|
|
75
92
|
end
|
|
76
93
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Seccomp
|
|
4
4
|
module Notify
|
|
5
5
|
class Policy
|
|
6
|
-
BOOTSTRAP_SYSCALLS = %i[fcntl
|
|
6
|
+
BOOTSTRAP_SYSCALLS = %i[fcntl].freeze
|
|
7
7
|
ALLOWED_FLAGS = Constants::SECCOMP_FILTER_FLAG_LOG |
|
|
8
8
|
Constants::SECCOMP_FILTER_FLAG_SPEC_ALLOW |
|
|
9
9
|
Constants::SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV
|
|
@@ -21,6 +21,7 @@ module Seccomp
|
|
|
21
21
|
|
|
22
22
|
@flags = flags
|
|
23
23
|
@notifications = []
|
|
24
|
+
@conditional_notifications = []
|
|
24
25
|
@denials = {}
|
|
25
26
|
deny_io_uring! if deny_io_uring
|
|
26
27
|
instance_eval(&block) if block
|
|
@@ -34,6 +35,14 @@ module Seccomp
|
|
|
34
35
|
@notifications.concat(names.map(&:to_sym))
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# Notifies only when one of the low 32 bits in a syscall argument is set.
|
|
39
|
+
def notify_if(name, argument:, mask:)
|
|
40
|
+
raise ArgumentError, "argument must be between 0 and 5" unless (0..5).cover?(argument)
|
|
41
|
+
raise ArgumentError, "mask must be between 1 and 0xffffffff" unless mask.is_a?(Integer) && (1..0xffff_ffff).cover?(mask)
|
|
42
|
+
|
|
43
|
+
@conditional_notifications << [name.to_sym, argument, mask]
|
|
44
|
+
end
|
|
45
|
+
|
|
37
46
|
# Rejects syscalls in the kernel without notifying the supervisor.
|
|
38
47
|
# @param names [Array<Symbol>]
|
|
39
48
|
# @param errno [Class, Integer] an Errno class or numeric errno
|
|
@@ -69,9 +78,18 @@ module Seccomp
|
|
|
69
78
|
@notifications.include?(name.to_sym)
|
|
70
79
|
end
|
|
71
80
|
|
|
81
|
+
def conditional_notifications(arch)
|
|
82
|
+
@conditional_notifications.filter_map do |name, argument, mask|
|
|
83
|
+
[Syscalls.number(name, arch), argument, mask] unless notified?(name) || @denials.key?(name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
private
|
|
73
88
|
|
|
74
89
|
def validate!
|
|
90
|
+
conflict = (@notifications + @conditional_notifications.map(&:first)).find { |name| @denials.key?(name) }
|
|
91
|
+
raise InvalidPolicyError, "#{conflict} cannot be both notified and denied" if conflict
|
|
92
|
+
|
|
75
93
|
invalid_bootstrap = BOOTSTRAP_SYSCALLS.find { |name| notified?(name) || @denials.key?(name) }
|
|
76
94
|
if invalid_bootstrap
|
|
77
95
|
raise InvalidPolicyError, "#{invalid_bootstrap} must be allowed while transferring the listener fd"
|
|
@@ -79,6 +79,10 @@ module Seccomp
|
|
|
79
79
|
# Emulates a successful syscall return.
|
|
80
80
|
# @return [void]
|
|
81
81
|
def allow!(value = 0)
|
|
82
|
+
unless value.is_a?(Integer) && (-(1 << 63)...(1 << 63)).cover?(value) && !(-Constants::MAX_ERRNO..-1).cover?(value)
|
|
83
|
+
raise ArgumentError, "value must be a signed 64-bit integer outside the errno range"
|
|
84
|
+
end
|
|
85
|
+
|
|
82
86
|
respond!(value:, error: 0, flags: 0)
|
|
83
87
|
end
|
|
84
88
|
|
|
@@ -130,14 +134,20 @@ module Seccomp
|
|
|
130
134
|
|
|
131
135
|
def kill!(signal = "KILL")
|
|
132
136
|
ensure_unresponded!
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
signal = Signal.list.fetch(signal.to_s.delete_prefix("SIG")) { raise ArgumentError, "unknown signal: #{signal.inspect}" } unless signal.is_a?(Integer)
|
|
138
|
+
tgid = Integer(File.read("/proc/#{@tid}/status")[/^Tgid:\s+(\d+)/, 1])
|
|
139
|
+
Libc.tgkill(tgid, @tid, signal)
|
|
140
|
+
error!
|
|
135
141
|
end
|
|
136
142
|
|
|
137
143
|
def mark_pointer_read!
|
|
138
144
|
@pointer_read = true
|
|
139
145
|
end
|
|
140
146
|
|
|
147
|
+
def close
|
|
148
|
+
@memory.close
|
|
149
|
+
end
|
|
150
|
+
|
|
141
151
|
private
|
|
142
152
|
|
|
143
153
|
def validate_id(request, buffer)
|
|
@@ -53,9 +53,8 @@ module Seccomp
|
|
|
53
53
|
@handlers = {}
|
|
54
54
|
@unknown_handler = ->(request) { request.error!(@default_errno) }
|
|
55
55
|
@error_handler = ->(error, _request) { warn("seccomp-notify supervisor: #{error.full_message(highlight: false, order: :top)}") }
|
|
56
|
-
@memory_cache = {}
|
|
57
|
-
@memory_cache_mutex = Mutex.new
|
|
58
56
|
@receive_mutex = Mutex.new
|
|
57
|
+
@state_mutex = Mutex.new
|
|
59
58
|
@stop = false
|
|
60
59
|
@listener_closed = false
|
|
61
60
|
end
|
|
@@ -97,19 +96,18 @@ module Seccomp
|
|
|
97
96
|
def run
|
|
98
97
|
workers = Array.new(@concurrency) { Thread.new { event_loop } }
|
|
99
98
|
status = wait_for_target
|
|
100
|
-
wait_until_listener_closes if @target_pid &&
|
|
101
|
-
|
|
99
|
+
wait_until_listener_closes if @target_pid && !stopped?
|
|
100
|
+
stop
|
|
102
101
|
workers.each(&:join)
|
|
103
102
|
status
|
|
104
103
|
ensure
|
|
105
|
-
|
|
106
|
-
@memory_cache.each_value { |io| io.close unless io.closed? }
|
|
104
|
+
stop
|
|
107
105
|
@listener.close unless @listener.closed?
|
|
108
106
|
@health_writer&.close unless @health_writer&.closed?
|
|
109
107
|
end
|
|
110
108
|
|
|
111
109
|
def stop
|
|
112
|
-
@stop = true
|
|
110
|
+
@state_mutex.synchronize { @stop = true }
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
private
|
|
@@ -118,7 +116,7 @@ module Seccomp
|
|
|
118
116
|
return wait_until_listener_closes unless @target_pid
|
|
119
117
|
|
|
120
118
|
loop do
|
|
121
|
-
return if
|
|
119
|
+
return if stopped?
|
|
122
120
|
|
|
123
121
|
waited = Process.waitpid2(@target_pid, Process::WNOHANG)
|
|
124
122
|
return waited.last if waited
|
|
@@ -132,7 +130,8 @@ module Seccomp
|
|
|
132
130
|
|
|
133
131
|
def wait_for_non_child
|
|
134
132
|
loop do
|
|
135
|
-
return if
|
|
133
|
+
return if stopped?
|
|
134
|
+
return if target_zombie?
|
|
136
135
|
|
|
137
136
|
Process.kill(0, @target_pid)
|
|
138
137
|
sleep(@poll_interval)
|
|
@@ -141,17 +140,23 @@ module Seccomp
|
|
|
141
140
|
nil
|
|
142
141
|
end
|
|
143
142
|
|
|
143
|
+
def target_zombie?
|
|
144
|
+
File.read("/proc/#{@target_pid}/stat").match?(/\A\d+ \(.*\) [ZX] /)
|
|
145
|
+
rescue Errno::ENOENT
|
|
146
|
+
true
|
|
147
|
+
end
|
|
148
|
+
|
|
144
149
|
def wait_until_listener_closes
|
|
145
|
-
sleep(@poll_interval) until
|
|
150
|
+
sleep(@poll_interval) until stopped? || listener_closed?
|
|
146
151
|
end
|
|
147
152
|
|
|
148
153
|
def event_loop
|
|
149
|
-
process_one until
|
|
154
|
+
process_one until stopped? || listener_closed?
|
|
150
155
|
rescue IOError, Errno::EBADF, Errno::ENOTCONN
|
|
151
|
-
|
|
156
|
+
stop
|
|
152
157
|
rescue StandardError => error
|
|
153
158
|
warn("seccomp-notify worker failed: #{error.message}")
|
|
154
|
-
|
|
159
|
+
stop
|
|
155
160
|
end
|
|
156
161
|
|
|
157
162
|
def process_one
|
|
@@ -161,13 +166,20 @@ module Seccomp
|
|
|
161
166
|
dispatch(request)
|
|
162
167
|
rescue Errno::EINTR
|
|
163
168
|
retry
|
|
169
|
+
rescue Errno::EAGAIN
|
|
170
|
+
mark_listener_closed if Libc.poll_hup?(@listener.fileno)
|
|
171
|
+
nil
|
|
164
172
|
rescue Errno::ENOENT
|
|
165
|
-
|
|
173
|
+
mark_listener_closed if Libc.poll_hup?(@listener.fileno)
|
|
166
174
|
nil
|
|
167
175
|
end
|
|
168
176
|
|
|
169
177
|
def receive_request
|
|
170
178
|
return unless IO.select([@listener], nil, nil, @poll_interval)
|
|
179
|
+
if Libc.poll_hup?(@listener.fileno)
|
|
180
|
+
mark_listener_closed
|
|
181
|
+
return
|
|
182
|
+
end
|
|
171
183
|
|
|
172
184
|
buffer = "\0" * @sizes[:notif]
|
|
173
185
|
Ioctl.call(@listener, Ioctl::NOTIF_RECV, buffer)
|
|
@@ -175,8 +187,8 @@ module Seccomp
|
|
|
175
187
|
buffer,
|
|
176
188
|
listener: @listener,
|
|
177
189
|
sizes: @sizes,
|
|
178
|
-
memory_cache:
|
|
179
|
-
memory_cache_mutex:
|
|
190
|
+
memory_cache: nil,
|
|
191
|
+
memory_cache_mutex: nil,
|
|
180
192
|
features: @features
|
|
181
193
|
)
|
|
182
194
|
end
|
|
@@ -191,7 +203,23 @@ module Seccomp
|
|
|
191
203
|
warn("seccomp-notify error callback failed: #{callback_error.message}")
|
|
192
204
|
end
|
|
193
205
|
ensure
|
|
194
|
-
|
|
206
|
+
begin
|
|
207
|
+
request&.error!(@default_errno) unless request&.responded?
|
|
208
|
+
ensure
|
|
209
|
+
request&.close
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def stopped?
|
|
214
|
+
@state_mutex.synchronize { @stop }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def listener_closed?
|
|
218
|
+
@state_mutex.synchronize { @listener_closed }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def mark_listener_closed
|
|
222
|
+
@state_mutex.synchronize { @listener_closed = true }
|
|
195
223
|
end
|
|
196
224
|
end
|
|
197
225
|
end
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ipaddr"
|
|
4
3
|
require "socket"
|
|
4
|
+
require "ipaddr"
|
|
5
5
|
|
|
6
6
|
module Seccomp
|
|
7
7
|
module Notify
|
|
8
8
|
class TargetMemory
|
|
9
9
|
PAGE_SIZE = 4096
|
|
10
|
+
SOCKADDR_STORAGE_SIZE = 128
|
|
10
11
|
|
|
11
|
-
def initialize(listener, request,
|
|
12
|
+
def initialize(listener, request, _cache = nil, _cache_mutex = nil)
|
|
12
13
|
@listener = listener
|
|
13
14
|
@request = request
|
|
14
|
-
@
|
|
15
|
-
|
|
15
|
+
@memory = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def close
|
|
19
|
+
@memory&.close unless @memory&.closed?
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def read(address, length)
|
|
@@ -24,7 +28,7 @@ module Seccomp
|
|
|
24
28
|
|
|
25
29
|
@request.mark_pointer_read!
|
|
26
30
|
data
|
|
27
|
-
rescue Errno::EIO, Errno::EFAULT, Errno::ENOENT, Errno::EACCES => error
|
|
31
|
+
rescue EOFError, Errno::EIO, Errno::EFAULT, Errno::ENOENT, Errno::EACCES => error
|
|
28
32
|
raise MemoryReadError, error.message
|
|
29
33
|
end
|
|
30
34
|
|
|
@@ -45,18 +49,22 @@ module Seccomp
|
|
|
45
49
|
|
|
46
50
|
def read_sockaddr(address, length)
|
|
47
51
|
raise MemoryReadError, "sockaddr is shorter than sa_family" if length < 2
|
|
52
|
+
raise MemoryReadError, "sockaddr is longer than sockaddr_storage" if length > SOCKADDR_STORAGE_SIZE
|
|
48
53
|
|
|
49
54
|
bytes = read(address, length)
|
|
50
55
|
case bytes.unpack1("S<")
|
|
51
56
|
when Socket::AF_INET
|
|
52
57
|
raise MemoryReadError, "short AF_INET sockaddr" if length < 16
|
|
53
|
-
|
|
58
|
+
port = bytes.unpack1("@2n")
|
|
59
|
+
host = IPAddr.ntop(bytes.byteslice(4, 4))
|
|
60
|
+
Addrinfo.new(Socket.sockaddr_in(port, host), Socket::AF_INET, 0, 0)
|
|
54
61
|
when Socket::AF_INET6
|
|
55
62
|
raise MemoryReadError, "short AF_INET6 sockaddr" if length < 28
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
port = bytes.unpack1("@2n")
|
|
64
|
+
host = IPAddr.ntop(bytes.byteslice(8, 16))
|
|
65
|
+
scope = bytes.unpack1("@24L<")
|
|
66
|
+
host = "#{host}%#{scope}" unless scope.zero?
|
|
67
|
+
Addrinfo.new(Socket.sockaddr_in(port, host), Socket::AF_INET6, 0, 0)
|
|
60
68
|
when Socket::AF_UNIX
|
|
61
69
|
raw_path = bytes.byteslice(2..).to_s
|
|
62
70
|
path = raw_path.start_with?("\0") ? raw_path : raw_path.split("\0", 2).first
|
|
@@ -69,9 +77,7 @@ module Seccomp
|
|
|
69
77
|
private
|
|
70
78
|
|
|
71
79
|
def memory
|
|
72
|
-
@
|
|
73
|
-
@cache[@request.tid] ||= File.open("/proc/#{@request.tid}/mem", File::RDONLY)
|
|
74
|
-
end
|
|
80
|
+
@memory ||= File.open("/proc/#{@request.tid}/mem", File::RDONLY)
|
|
75
81
|
end
|
|
76
82
|
end
|
|
77
83
|
end
|
data/lib/seccomp/notify.rb
CHANGED
|
@@ -62,16 +62,17 @@ module Seccomp
|
|
|
62
62
|
raise ArgumentError, "recv_timeout must be positive"
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
program, install_flags = prepare_supervision(policy, supervisor_options)
|
|
66
65
|
parent_socket, child_socket = UNIXSocket.pair
|
|
66
|
+
program, install_flags = prepare_supervision(policy, supervisor_options, transfer_fd: child_socket.fileno)
|
|
67
67
|
health_reader, health_writer = IO.pipe
|
|
68
68
|
pid = fork do
|
|
69
69
|
parent_socket.close
|
|
70
70
|
health_writer.close
|
|
71
71
|
keep_health_reader(health_reader)
|
|
72
|
-
|
|
73
|
-
FdPassing.send_fd(child_socket,
|
|
74
|
-
child_socket
|
|
72
|
+
listener_fd = Filter.install!(program, flags: install_flags, raw: true)
|
|
73
|
+
FdPassing.send_fd(child_socket, listener_fd)
|
|
74
|
+
keep_transfer_socket(child_socket)
|
|
75
|
+
listener = IO.for_fd(listener_fd, "r")
|
|
75
76
|
listener.close
|
|
76
77
|
target.call
|
|
77
78
|
exit! 0
|
|
@@ -102,11 +103,11 @@ module Seccomp
|
|
|
102
103
|
raise ArgumentError, "supervisor must be :spawn or :fork" unless %i[spawn fork].include?(supervisor)
|
|
103
104
|
raise ArgumentError, "supervisor: :spawn does not support Ruby handler blocks" if supervisor == :spawn && configure
|
|
104
105
|
|
|
105
|
-
program, install_flags = prepare_supervision(policy, options)
|
|
106
106
|
selected_features = options.fetch(:features, features)
|
|
107
107
|
raise NotSupportedError, "supervise_self requires CONTINUE when no handler block is given" if !configure && !selected_features[:continue]
|
|
108
108
|
|
|
109
109
|
parent_socket, supervisor_socket = UNIXSocket.pair
|
|
110
|
+
program, install_flags = prepare_supervision(policy, options, transfer_fd: parent_socket.fileno)
|
|
110
111
|
health_reader, health_writer = IO.pipe
|
|
111
112
|
target_pid = Process.pid
|
|
112
113
|
supervisor_pid = case supervisor
|
|
@@ -119,10 +120,11 @@ module Seccomp
|
|
|
119
120
|
supervisor_socket.close
|
|
120
121
|
health_writer.close
|
|
121
122
|
keep_health_reader(health_reader)
|
|
122
|
-
|
|
123
|
-
FdPassing.send_fd(parent_socket,
|
|
123
|
+
listener_fd = Filter.install!(program, flags: install_flags, raw: true)
|
|
124
|
+
FdPassing.send_fd(parent_socket, listener_fd)
|
|
125
|
+
keep_transfer_socket(parent_socket)
|
|
126
|
+
listener = IO.for_fd(listener_fd, "r")
|
|
124
127
|
listener.close
|
|
125
|
-
parent_socket.close
|
|
126
128
|
Process.detach(supervisor_pid)
|
|
127
129
|
supervisor_pid
|
|
128
130
|
rescue StandardError
|
|
@@ -175,14 +177,14 @@ module Seccomp
|
|
|
175
177
|
)
|
|
176
178
|
end
|
|
177
179
|
|
|
178
|
-
def prepare_supervision(policy, options)
|
|
180
|
+
def prepare_supervision(policy, options, transfer_fd: nil)
|
|
179
181
|
Supervisor.validate_options!(options)
|
|
180
182
|
sizes = notif_sizes
|
|
181
183
|
raise NotSupportedError, "unsupported kernel notification sizes: #{sizes.inspect}" unless sizes == Supervisor::EXPECTED_SIZES
|
|
182
184
|
|
|
183
185
|
selected_features = options.fetch(:features, features)
|
|
184
186
|
optional_flags = selected_features[:wait_killable_recv] ? Constants::SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV : 0
|
|
185
|
-
[BPF::Builder.new(policy).build, policy.flags | optional_flags]
|
|
187
|
+
[BPF::Builder.new(policy, transfer_fd:).build, policy.flags | optional_flags]
|
|
186
188
|
end
|
|
187
189
|
|
|
188
190
|
def keep_health_reader(reader)
|
|
@@ -191,6 +193,11 @@ module Seccomp
|
|
|
191
193
|
@supervisor_health_reader = reader
|
|
192
194
|
end
|
|
193
195
|
|
|
196
|
+
def keep_transfer_socket(socket)
|
|
197
|
+
socket.close_on_exec = false
|
|
198
|
+
@listener_transfer_socket = socket
|
|
199
|
+
end
|
|
200
|
+
|
|
194
201
|
def supervisor_health_reader
|
|
195
202
|
return @supervisor_health_reader if defined?(@supervisor_health_reader)
|
|
196
203
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: seccomp-notify
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -60,12 +60,12 @@ files:
|
|
|
60
60
|
- lib/seccomp/notify/version.rb
|
|
61
61
|
- spike/notify_min.c
|
|
62
62
|
- tools/gen_syscall_table.rb
|
|
63
|
-
homepage: https://
|
|
63
|
+
homepage: https://ydah.github.io/seccomp-notify/
|
|
64
64
|
licenses:
|
|
65
65
|
- MIT
|
|
66
66
|
metadata:
|
|
67
67
|
rubygems_mfa_required: 'true'
|
|
68
|
-
homepage_uri: https://
|
|
68
|
+
homepage_uri: https://ydah.github.io/seccomp-notify/
|
|
69
69
|
rdoc_options: []
|
|
70
70
|
require_paths:
|
|
71
71
|
- lib
|
|
@@ -80,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0'
|
|
82
82
|
requirements: []
|
|
83
|
-
rubygems_version: 4.0.
|
|
83
|
+
rubygems_version: 4.0.19
|
|
84
84
|
specification_version: 4
|
|
85
85
|
summary: Pure Ruby bindings for Linux seccomp user notifications
|
|
86
86
|
test_files: []
|