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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4348459a8a4722dfdc17dcf0acf21b0e7802f6b9b5fe80e1821d9cb1964d7802
4
- data.tar.gz: 3d8776d6b56396439b30b17897eb862dde5fbe8c591ce3e58d62a1c928c90e72
3
+ metadata.gz: ace169c2d9c0677fc63afda27695ec1afcf8dbfa2ee0b7e935690ee1b48c9c13
4
+ data.tar.gz: 159c4ea512b51d37cef181ba13cfc95b176b7ad5044ab1131cdc626ac281f419
5
5
  SHA512:
6
- metadata.gz: b3c6841b95077468dea06d50250d91f660386cf21e33240bcf270b07b7f7efabe6e890a0ee1e6ca22d6492d9cb6837b1cd25954c4eb93d195c77a4ce025221ac
7
- data.tar.gz: 63d5fdf903dc3541c9ded5eb90deb66de7ea4171304b375f6228aae690462816cd5880eef05de9d27df95bb843fb84c573e89b6ffc718cee50c2e693827ff731
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
- # seccomp-notify
2
-
3
- Pure Ruby plumbing for Linux [`SECCOMP_RET_USER_NOTIF`](https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html). It builds cBPF filters, transfers listener file descriptors, supervises notifications, reads target memory, and injects file descriptors without a native extension.
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 argument and then use `continue!` as a security decision: another target thread can change that memory between inspection and syscall execution (TOCTOU). Reject the syscall or emulate it instead.
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
- Seccomp cannot intercept vDSO calls, I/O submitted through an existing `io_uring`, operations on already-open file descriptors, or indirect shared-memory effects. `io_uring_setup` is denied by default. If the supervisor dies, blocked notification syscalls return `ENOSYS`.
48
+ ## Installation
9
49
 
10
- `fcntl` and `sendmsg` must remain allowed because Ruby uses them to wrap and transfer the listener fd; policies that try to notify or deny them are rejected.
50
+ Add the gem to your Gemfile:
11
51
 
12
- ## Requirements
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 needs 5.9 or newer
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 isolated test container with:
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
- ## Installation
26
-
27
- ```ruby
28
- gem "seccomp-notify"
29
- ```
30
-
31
- ## Usage
76
+ ## Quick Start
32
77
 
33
- The recommended shape keeps the unfiltered supervisor in the parent and installs the filter in a child:
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(*open_syscalls, :connect)
84
+ notify :connect, :sendto
41
85
  end
42
86
 
43
87
  supervisor = Seccomp::Notify.spawn(policy) do
44
- exec("bundle", "install")
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 syscall, not the process ID.
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
- `supervise_self(policy, supervisor: :fork)` accepts a block that configures the child supervisor. The safer `:spawn` form starts a clean Ruby VM and therefore supports only the default pass-through handler. Both forms detach the supervisor child, so broad `Process.wait` calls in the target can still observe related lifecycle effects.
124
+ ## How It Works
65
125
 
66
- ## Responses and features
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
- - `allow!(value = 0)` emulates a successful return value without executing the syscall.
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
- Use `Seccomp::Notify.features` to inspect runtime support. Set `SECCOMP_NOTIFY_DISABLE_FEATURES=addfd,continue` to force feature fallbacks in tests.
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
- Targets started by this gem can call `Seccomp::Notify.supervisor_alive?` to poll the health pipe. If the target `exec`s, the pipe fd is inherited and published as `SECCOMP_NOTIFY_HEALTH_FD`.
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
- Run `bundle exec rake`. Linux runs include real seccomp integration tests; other systems run only portable filter and layout tests. `spike/notify_min.c` is the reference C round trip, and `tools/gen_syscall_table.rb` regenerates architecture tables from Linux kernel headers.
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
- Releases before 1.0 may change the API. The project is available under the MIT License.
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 = 64
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
- Program.new(prologue + body)
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
@@ -9,7 +9,7 @@ module Seccomp
9
9
  module_function
10
10
 
11
11
  def send_fd(socket, io)
12
- socket.sendmsg("\0", 0, nil, Socket::AncillaryData.unix_rights(io))
12
+ socket.send_io(io)
13
13
  end
14
14
 
15
15
  def recv_fd(socket, timeout: 10)
@@ -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
@@ -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
- (buffer.unpack1("@6S<") & 0x10).positive?
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 sendmsg].freeze
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!
@@ -138,6 +138,10 @@ module Seccomp
138
138
  @pointer_read = true
139
139
  end
140
140
 
141
+ def close
142
+ @memory.close
143
+ end
144
+
141
145
  private
142
146
 
143
147
  def validate_id(request, buffer)
@@ -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: @memory_cache,
179
- memory_cache_mutex: @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
- request&.error!(@default_errno) unless request&.responded?
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, cache, cache_mutex = Mutex.new)
12
+ def initialize(listener, request, _cache = nil, _cache_mutex = nil)
12
13
  @listener = listener
13
14
  @request = request
14
- @cache = cache
15
- @cache_mutex = cache_mutex
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
- @cache[@request.tid] || @cache_mutex.synchronize do
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Seccomp
4
4
  module Notify
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -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
- listener = Filter.install!(program, flags: install_flags)
73
- FdPassing.send_fd(child_socket, listener)
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
- listener = Filter.install!(program, flags: install_flags)
123
- FdPassing.send_fd(parent_socket, listener)
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.1.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://rubygems.org/gems/seccomp-notify
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://rubygems.org/gems/seccomp-notify
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.6
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: []