seccomp-notify 0.1.0 → 0.2.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 +5 -0
- data/README.md +130 -43
- data/lib/seccomp/notify/bpf/builder.rb +48 -3
- data/lib/seccomp/notify/constants.rb +2 -0
- data/lib/seccomp/notify/fd_passing.rb +1 -1
- data/lib/seccomp/notify/filter.rb +2 -2
- data/lib/seccomp/notify/libc.rb +3 -3
- data/lib/seccomp/notify/policy.rb +16 -1
- data/lib/seccomp/notify/request.rb +4 -0
- data/lib/seccomp/notify/supervisor.rb +21 -6
- data/lib/seccomp/notify/target_memory.rb +10 -7
- data/lib/seccomp/notify/version.rb +1 -1
- data/lib/seccomp/notify.rb +10 -8
- 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: ace169c2d9c0677fc63afda27695ec1afcf8dbfa2ee0b7e935690ee1b48c9c13
|
|
4
|
+
data.tar.gz: 159c4ea512b51d37cef181ba13cfc95b176b7ad5044ab1131cdc626ac281f419
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6875777a81e6507e7ea582ecfe3078b193c9dda9a896e424112e1976c3ee6bd9e943f41294bed8dd49e50cb2a8aa91f9560c3093ad49700402b154c4c1af627b
|
|
7
|
+
data.tar.gz: 0cb483dd0df1793bf30df385c73bd14daf2c30ed81dabcbe714e8ecdbc56c84308386b9c277145c206bc2543465370ec080b11e8ebefdc39d048aab8c56e4133
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ 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.2.0 - 2026-08-22
|
|
9
|
+
|
|
10
|
+
- Stop supervising a non-child target once it becomes a zombie.
|
|
11
|
+
- Read target memory from the current address space after `exec` and normalize EOF failures.
|
|
12
|
+
|
|
8
13
|
## v0.1.0 - 2026-08-22
|
|
9
14
|
|
|
10
15
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,82 +1,169 @@
|
|
|
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.
|
|
106
|
+
|
|
107
|
+
## Responses
|
|
108
|
+
|
|
109
|
+
| Method | Behavior | Requirement |
|
|
110
|
+
|--------|----------|-------------|
|
|
111
|
+
| `allow!(value = 0)` | Emulate a successful return value without running the syscall | Linux 5.0+ |
|
|
112
|
+
| `error!(Errno::EPERM)` | Return an errno | Linux 5.0+ |
|
|
113
|
+
| `continue!` | Run the original syscall | Linux 5.5+ |
|
|
114
|
+
| `add_fd!(io)` | Inject a file descriptor and return its number | Linux 5.9+ |
|
|
115
|
+
| `kill!` | Terminate the issuing process | — |
|
|
116
|
+
|
|
117
|
+
Use `Seccomp::Notify.features` to inspect runtime support. Set
|
|
118
|
+
`SECCOMP_NOTIFY_DISABLE_FEATURES=addfd,continue` to force feature fallbacks in tests.
|
|
119
|
+
|
|
120
|
+
Targets started by this gem can call `Seccomp::Notify.supervisor_alive?` to poll the health pipe.
|
|
121
|
+
If the target calls `exec`, the pipe descriptor is inherited and published as
|
|
122
|
+
`SECCOMP_NOTIFY_HEALTH_FD`.
|
|
63
123
|
|
|
64
|
-
|
|
124
|
+
## How It Works
|
|
65
125
|
|
|
66
|
-
|
|
126
|
+
1. The parent creates a Unix socket pair and forks the target.
|
|
127
|
+
2. The target installs a cBPF filter and sends its listener descriptor to the parent with
|
|
128
|
+
`SCM_RIGHTS`.
|
|
129
|
+
3. The parent receives syscall notifications and dispatches them to registered handlers.
|
|
130
|
+
4. Each handler returns an errno, emulated value, continuation, or injected descriptor.
|
|
67
131
|
|
|
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.
|
|
132
|
+
## Security
|
|
73
133
|
|
|
74
|
-
|
|
134
|
+
- Pointer data may change after inspection. Never use it to justify `continue!`.
|
|
135
|
+
- vDSO calls, existing file descriptors, and indirect shared-memory effects are outside seccomp's
|
|
136
|
+
control.
|
|
137
|
+
- I/O submitted through an existing `io_uring` does not pass through the filter;
|
|
138
|
+
`io_uring_setup` is denied by default.
|
|
139
|
+
- If the supervisor dies, blocked notification syscalls return `ENOSYS`.
|
|
140
|
+
- `fcntl` and `sendmsg` must remain allowed because Ruby uses them to wrap and transfer the listener
|
|
141
|
+
descriptor. Policies that notify or deny them are rejected.
|
|
75
142
|
|
|
76
|
-
|
|
143
|
+
## Examples
|
|
144
|
+
|
|
145
|
+
- [`examples/deny_network.rb`](examples/deny_network.rb) rejects outbound network syscalls.
|
|
146
|
+
- [`examples/emulate_open.rb`](examples/emulate_open.rb) replaces `/dev/urandom` with an injected
|
|
147
|
+
file descriptor.
|
|
148
|
+
- [`examples/strace_lite.rb`](examples/strace_lite.rb) logs selected syscall notifications.
|
|
77
149
|
|
|
78
150
|
## Development
|
|
79
151
|
|
|
80
|
-
|
|
152
|
+
```sh
|
|
153
|
+
bundle install
|
|
154
|
+
bundle exec rake
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Linux runs include real seccomp integration tests; other systems run portable filter and layout
|
|
158
|
+
tests only. `spike/notify_min.c` is the reference C round trip, and
|
|
159
|
+
`tools/gen_syscall_table.rb` regenerates architecture tables from Linux kernel syscall tables.
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
Bug reports and pull requests are welcome at https://github.com/ydah/seccomp-notify.
|
|
164
|
+
|
|
165
|
+
Releases before 1.0 may change the API.
|
|
166
|
+
|
|
167
|
+
## License
|
|
81
168
|
|
|
82
|
-
|
|
169
|
+
Released under the [MIT License](LICENSE.txt).
|
|
@@ -4,20 +4,29 @@ 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
|
|
@@ -37,6 +46,42 @@ module Seccomp
|
|
|
37
46
|
end << ins(Constants::BPF_RET_K, 0, 0, @policy.default_action)
|
|
38
47
|
end
|
|
39
48
|
|
|
49
|
+
def transfer_allowance
|
|
50
|
+
return [] unless @transfer_fd
|
|
51
|
+
|
|
52
|
+
conditional(Syscalls.number(:sendmsg, @arch), 0, @transfer_fd, Constants::BPF_JMP_JEQ_K)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def conditional_notifications(conditions, decisions)
|
|
56
|
+
last_condition = {}
|
|
57
|
+
conditions.each_with_index { |(number, _argument, _mask), index| last_condition[number] = index }
|
|
58
|
+
conditions.each_with_index.flat_map do |(number, argument, mask), index|
|
|
59
|
+
fallback = decisions.fetch(number, @policy.default_action) if last_condition.fetch(number) == index
|
|
60
|
+
conditional(number, argument, mask, Constants::BPF_JMP_JSET_K, fallback:)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def conditional(number, argument, value, comparison, fallback: nil)
|
|
65
|
+
if fallback
|
|
66
|
+
return [
|
|
67
|
+
ins(Constants::BPF_JMP_JEQ_K, 0, 4, number),
|
|
68
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_ARGS + argument * 8),
|
|
69
|
+
ins(comparison, 0, 1, value),
|
|
70
|
+
ins(Constants::BPF_RET_K, 0, 0, Constants::SECCOMP_RET_USER_NOTIF),
|
|
71
|
+
ins(Constants::BPF_RET_K, 0, 0, fallback),
|
|
72
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_NR)
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
[
|
|
77
|
+
ins(Constants::BPF_JMP_JEQ_K, 0, 3, number),
|
|
78
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_ARGS + argument * 8),
|
|
79
|
+
ins(comparison, 0, 1, value),
|
|
80
|
+
ins(Constants::BPF_RET_K, 0, 0, comparison == Constants::BPF_JMP_JEQ_K ? Constants::SECCOMP_RET_ALLOW : Constants::SECCOMP_RET_USER_NOTIF),
|
|
81
|
+
ins(Constants::BPF_LD_W_ABS, 0, 0, Constants::OFF_NR)
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
|
|
40
85
|
def tree(decisions)
|
|
41
86
|
return [ins(Constants::BPF_RET_K, 0, 0, @policy.default_action)] if decisions.empty?
|
|
42
87
|
return linear(decisions) if decisions.length <= 4
|
|
@@ -35,10 +35,12 @@ module Seccomp
|
|
|
35
35
|
SYS_SECCOMP = {x86_64: 317, aarch64: 277}.freeze
|
|
36
36
|
|
|
37
37
|
PR_SET_NO_NEW_PRIVS = 38
|
|
38
|
+
PR_SET_PTRACER = 0x5961_6d61
|
|
38
39
|
BPF_LD_W_ABS = 0x20
|
|
39
40
|
BPF_JMP_JA = 0x05
|
|
40
41
|
BPF_JMP_JEQ_K = 0x15
|
|
41
42
|
BPF_JMP_JGE_K = 0x35
|
|
43
|
+
BPF_JMP_JSET_K = 0x45
|
|
42
44
|
BPF_RET_K = 0x06
|
|
43
45
|
BPF_MAXINSNS = 4096
|
|
44
46
|
MAX_ERRNO = 4095
|
|
@@ -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"],
|
|
@@ -60,7 +59,8 @@ module Seccomp
|
|
|
60
59
|
result = POLL.call(Fiddle::Pointer[buffer], 1, 0)
|
|
61
60
|
raise SystemCallError.new("poll", Fiddle.last_error) if result.negative?
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
revents = buffer.unpack1("@6S<")
|
|
63
|
+
(revents & 0x10).positive? && (revents & 0x01).zero?
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def notif_sizes
|
|
@@ -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,6 +78,12 @@ 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!
|
|
@@ -53,8 +53,6 @@ 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
|
|
59
57
|
@stop = false
|
|
60
58
|
@listener_closed = false
|
|
@@ -103,7 +101,6 @@ module Seccomp
|
|
|
103
101
|
status
|
|
104
102
|
ensure
|
|
105
103
|
@stop = true
|
|
106
|
-
@memory_cache.each_value { |io| io.close unless io.closed? }
|
|
107
104
|
@listener.close unless @listener.closed?
|
|
108
105
|
@health_writer&.close unless @health_writer&.closed?
|
|
109
106
|
end
|
|
@@ -133,6 +130,7 @@ module Seccomp
|
|
|
133
130
|
def wait_for_non_child
|
|
134
131
|
loop do
|
|
135
132
|
return if @stop
|
|
133
|
+
return if target_zombie?
|
|
136
134
|
|
|
137
135
|
Process.kill(0, @target_pid)
|
|
138
136
|
sleep(@poll_interval)
|
|
@@ -141,6 +139,12 @@ module Seccomp
|
|
|
141
139
|
nil
|
|
142
140
|
end
|
|
143
141
|
|
|
142
|
+
def target_zombie?
|
|
143
|
+
File.read("/proc/#{@target_pid}/stat").match?(/\A\d+ \(.*\) [ZX] /)
|
|
144
|
+
rescue Errno::ENOENT
|
|
145
|
+
true
|
|
146
|
+
end
|
|
147
|
+
|
|
144
148
|
def wait_until_listener_closes
|
|
145
149
|
sleep(@poll_interval) until @stop || @listener_closed
|
|
146
150
|
end
|
|
@@ -161,6 +165,9 @@ module Seccomp
|
|
|
161
165
|
dispatch(request)
|
|
162
166
|
rescue Errno::EINTR
|
|
163
167
|
retry
|
|
168
|
+
rescue Errno::EAGAIN
|
|
169
|
+
@listener_closed = true if Libc.poll_hup?(@listener.fileno)
|
|
170
|
+
nil
|
|
164
171
|
rescue Errno::ENOENT
|
|
165
172
|
@listener_closed = true if Libc.poll_hup?(@listener.fileno)
|
|
166
173
|
nil
|
|
@@ -168,6 +175,10 @@ module Seccomp
|
|
|
168
175
|
|
|
169
176
|
def receive_request
|
|
170
177
|
return unless IO.select([@listener], nil, nil, @poll_interval)
|
|
178
|
+
if Libc.poll_hup?(@listener.fileno)
|
|
179
|
+
@listener_closed = true
|
|
180
|
+
return
|
|
181
|
+
end
|
|
171
182
|
|
|
172
183
|
buffer = "\0" * @sizes[:notif]
|
|
173
184
|
Ioctl.call(@listener, Ioctl::NOTIF_RECV, buffer)
|
|
@@ -175,8 +186,8 @@ module Seccomp
|
|
|
175
186
|
buffer,
|
|
176
187
|
listener: @listener,
|
|
177
188
|
sizes: @sizes,
|
|
178
|
-
memory_cache:
|
|
179
|
-
memory_cache_mutex:
|
|
189
|
+
memory_cache: nil,
|
|
190
|
+
memory_cache_mutex: nil,
|
|
180
191
|
features: @features
|
|
181
192
|
)
|
|
182
193
|
end
|
|
@@ -191,7 +202,11 @@ module Seccomp
|
|
|
191
202
|
warn("seccomp-notify error callback failed: #{callback_error.message}")
|
|
192
203
|
end
|
|
193
204
|
ensure
|
|
194
|
-
|
|
205
|
+
begin
|
|
206
|
+
request&.error!(@default_errno) unless request&.responded?
|
|
207
|
+
ensure
|
|
208
|
+
request&.close
|
|
209
|
+
end
|
|
195
210
|
end
|
|
196
211
|
end
|
|
197
212
|
end
|
|
@@ -7,12 +7,16 @@ 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,6 +49,7 @@ 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<")
|
|
@@ -69,9 +74,7 @@ module Seccomp
|
|
|
69
74
|
private
|
|
70
75
|
|
|
71
76
|
def memory
|
|
72
|
-
@
|
|
73
|
-
@cache[@request.tid] ||= File.open("/proc/#{@request.tid}/mem", File::RDONLY)
|
|
74
|
-
end
|
|
77
|
+
@memory ||= File.open("/proc/#{@request.tid}/mem", File::RDONLY)
|
|
75
78
|
end
|
|
76
79
|
end
|
|
77
80
|
end
|
data/lib/seccomp/notify.rb
CHANGED
|
@@ -62,15 +62,16 @@ 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,
|
|
72
|
+
listener_fd = Filter.install!(program, flags: install_flags, raw: true)
|
|
73
|
+
FdPassing.send_fd(child_socket, listener_fd)
|
|
74
|
+
listener = IO.for_fd(listener_fd, "r")
|
|
74
75
|
child_socket.close
|
|
75
76
|
listener.close
|
|
76
77
|
target.call
|
|
@@ -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,8 +120,9 @@ 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
|
+
listener = IO.for_fd(listener_fd, "r")
|
|
124
126
|
listener.close
|
|
125
127
|
parent_socket.close
|
|
126
128
|
Process.detach(supervisor_pid)
|
|
@@ -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)
|
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.2.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: []
|