pidfd 0.6.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 61a5d615dfec6deefe6cb78906b3be7887fb747696ac911c91dafabf5e123db5
4
+ data.tar.gz: 7a7a6d725b86d614ea72d61f14f19d9ad3b14a934eb5df16a7a434441d1b7224
5
+ SHA512:
6
+ metadata.gz: e7be3b9c2c7127e959ec114d9ae0d460be6ba618dc3b39c681f8e9915a03ff218f349a423571ad0e0ded86ce542469495406c32457c443ec91f498cc5efe1357
7
+ data.tar.gz: 2150b342587007ed488338afb0f5b1a818768d98faba2b3eb2a407d9ebf6753beb430d588c530ba6aa52bbe2bf84ddaa31afb2a80c5d7c4ceb8af1c48aab7e2d
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Pidfd Changelog
2
+
3
+ ## 0.6.0 (2025-09-05)
4
+ - [Breaking] **BREAKING CHANGE**: Moved main API from `Pidfd::Pidfd` to `Pidfd` class
5
+ - Old: `pidfd = Pidfd::Pidfd.new(pid)` → New: `pidfd = Pidfd.new(pid)`
6
+ - Old: `Pidfd::Pidfd.supported?` → New: `Pidfd.supported?`
7
+ - Old: `Pidfd::Pidfd.pidfd_open_syscall = 434` → New: `Pidfd.pidfd_open_syscall = 434`
8
+ - Error classes remain namespaced as `Pidfd::Errors::*`
9
+ - This simplifies the API and removes redundant nested class structure
10
+
11
+ ## 0.5.0 (2025-09-05)
12
+ - [Feature] Initial release extracted from Karafka framework.
13
+ - [Feature] Core pidfd functionality for Linux 5.3+.
14
+ - [Feature] `Pidfd::Pidfd` class with process management capabilities.
15
+ - [Feature] `#alive?` method for race-free process status checking.
16
+ - [Feature] `#signal` method for safe signal delivery.
17
+ - [Feature] `#cleanup` method for zombie process reaping.
18
+ - [Feature] Platform support detection via `Pidfd::Pidfd.supported?`.
19
+ - [Feature] Configurable syscall numbers for different architectures.
20
+ - [Feature] Thread-safe operations with mutex protection.
21
+ - [Feature] Non-blocking process monitoring using IO.select.
22
+ - [Feature] Comprehensive error handling with custom exceptions.
23
+ - [Feature] Full test suite with RSpec.
24
+ - [Feature] GitHub Actions CI/CD pipeline.
25
+ - [Feature] Documentation and usage examples.
26
+ - [Enhancement] Uses FFI for direct syscall bindings.
27
+ - [Enhancement] Supports pidfd_open and pidfd_send_signal syscalls.
28
+ - [Enhancement] Implements waitid for process cleanup.
29
+ - [Enhancement] Compatible with Ruby 3.2+.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Maciej Mensfeld
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # Pidfd
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/pidfd.svg)](https://rubygems.org/gems/pidfd)
4
+ [![Build Status](https://github.com/mensfeld/pidfd/actions/workflows/ci.yml/badge.svg)](https://github.com/mensfeld/pidfd/actions/workflows/ci.yml)
5
+
6
+ A Ruby wrapper for Linux pidfd (Process File Descriptor) system calls, providing safer process management with guaranteed process identity.
7
+
8
+ ## What is pidfd?
9
+
10
+ Process file descriptors (pidfd) were introduced in Linux 5.3 (2019) to solve fundamental problems with traditional PID-based process management:
11
+
12
+ ### The Problem with PIDs
13
+
14
+ Traditional Unix PIDs have a critical flaw: **PID reuse**. When a process dies, its PID can be immediately reassigned to a new, unrelated process. This creates race conditions where:
15
+
16
+ - You might send signals to the wrong process
17
+ - Process state checks become unreliable
18
+ - Security vulnerabilities can arise from PID confusion
19
+
20
+ ### The pidfd Solution
21
+
22
+ Pidfds provide a **stable reference** to a process that remains valid even if the PID is reused. Key benefits:
23
+
24
+ - **Race-free signal delivery** - Signals always go to the intended process
25
+ - **Reliable process monitoring** - Know definitively when a process exits
26
+ - **Thread-safe operations** - No TOCTTOU races
27
+ - **Pollable file descriptors** - Integrate with event loops efficiently
28
+
29
+ ## When to Use This Gem
30
+
31
+ Use pidfd when you need:
32
+
33
+ - **Reliable process management** in production environments
34
+ - **Non-child process monitoring** without being the parent
35
+ - **High-security applications** where PID confusion is unacceptable
36
+ - **Systems with high process churn** where PID reuse is common
37
+ - **Libraries operating in uncontrolled environments**
38
+
39
+ Don't use pidfd for:
40
+ - macOS or Windows (Linux-only feature)
41
+ - Kernels older than Linux 5.3
42
+ - Simple parent-child relationships (traditional wait works fine)
43
+
44
+ ## Installation
45
+
46
+ Add this line to your application's Gemfile:
47
+
48
+ ```ruby
49
+ gem 'pidfd'
50
+ ```
51
+
52
+ And then execute:
53
+
54
+ ```bash
55
+ bundle install
56
+ ```
57
+
58
+ Or install it yourself as:
59
+
60
+ ```bash
61
+ gem install pidfd
62
+ ```
63
+
64
+ ## Requirements
65
+
66
+ - Linux kernel 5.3 or newer
67
+ - Ruby 3.2 or newer
68
+ - FFI gem
69
+
70
+ ## Usage
71
+
72
+ ### Basic Process Management
73
+
74
+ ```ruby
75
+ require 'pidfd'
76
+
77
+ # Create a pidfd for a process
78
+ process = fork { sleep 10 }
79
+ pidfd = Pidfd.new(process)
80
+
81
+ # Check if process is alive
82
+ pidfd.alive? # => true
83
+
84
+ # Send a signal safely
85
+ pidfd.signal('TERM') # => true (signal sent)
86
+
87
+ # Clean up zombie process after it exits
88
+ pidfd.cleanup
89
+ ```
90
+
91
+ ### Monitoring Non-Child Processes
92
+
93
+ ```ruby
94
+ # Monitor any process by PID (requires appropriate permissions)
95
+ nginx_pid = File.read('/var/run/nginx.pid').to_i
96
+ pidfd = Pidfd.new(nginx_pid)
97
+
98
+ # Check status without race conditions
99
+ if pidfd.alive?
100
+ puts "Nginx is running"
101
+ else
102
+ puts "Nginx has stopped"
103
+ pidfd.cleanup
104
+ end
105
+ ```
106
+
107
+ ### Safe Signal Delivery
108
+
109
+ ```ruby
110
+ # Traditional approach (UNSAFE - race condition)
111
+ Process.kill('TERM', pid) # Might hit wrong process if PID was reused!
112
+
113
+ # Pidfd approach (SAFE)
114
+ pidfd = Pidfd.new(pid)
115
+ pidfd.signal('TERM') # Guaranteed to hit the right process or fail safely
116
+ ```
117
+
118
+ ### Integration with Event Loops
119
+
120
+ ```ruby
121
+ # Pidfd provides a pollable file descriptor
122
+ pidfd = Pidfd.new(child_pid)
123
+
124
+ # Use with IO.select for non-blocking monitoring
125
+ loop do
126
+ if pidfd.alive?
127
+ # Process still running
128
+ sleep 0.1
129
+ else
130
+ # Process exited
131
+ pidfd.cleanup
132
+ break
133
+ end
134
+ end
135
+ ```
136
+
137
+ ## Platform Support
138
+
139
+ ### Checking Support
140
+
141
+ ```ruby
142
+ if Pidfd.supported?
143
+ puts "pidfd is supported on this system"
144
+ else
145
+ puts "pidfd is not available (wrong OS or kernel version)"
146
+ end
147
+ ```
148
+
149
+ ### Fallback Strategy
150
+
151
+ ```ruby
152
+ def safe_process_check(pid)
153
+ if Pidfd.supported?
154
+ pidfd = Pidfd.new(pid)
155
+ result = pidfd.alive?
156
+ pidfd.cleanup unless result
157
+ result
158
+ else
159
+ # Fallback to traditional (less safe) approach
160
+ Process.kill(0, pid)
161
+ true
162
+ rescue Errno::ESRCH
163
+ false
164
+ end
165
+ end
166
+ ```
167
+
168
+ ## Configuration
169
+
170
+ ### Custom Syscall Numbers
171
+
172
+ Different architectures may use different syscall numbers. You can configure them:
173
+
174
+ ```ruby
175
+ # Default values for x86_64
176
+ Pidfd.pidfd_open_syscall = 434
177
+ Pidfd.pidfd_signal_syscall = 424
178
+
179
+ # For other architectures, consult your system headers
180
+ ```
181
+
182
+ ## Error Handling
183
+
184
+ ```ruby
185
+ begin
186
+ pidfd = Pidfd.new(pid)
187
+ rescue Pidfd::Errors::PidfdOpenFailedError => e
188
+ # Process doesn't exist or permission denied
189
+ puts "Could not open pidfd: #{e.message}"
190
+ end
191
+
192
+ begin
193
+ pidfd.signal('TERM')
194
+ rescue Pidfd::Errors::PidfdSignalFailedError => e
195
+ # Signal delivery failed
196
+ puts "Could not send signal: #{e.message}"
197
+ end
198
+ ```
199
+
200
+ ## Performance Considerations
201
+
202
+ - **Efficient**: Pidfd operations are kernel-level, very fast
203
+ - **Low overhead**: Minimal memory usage per pidfd
204
+ - **Scalable**: Handles thousands of processes efficiently
205
+ - **Non-blocking**: Supports async operation patterns
206
+
207
+ ## Security
208
+
209
+ Pidfd provides significant security improvements:
210
+
211
+ - Prevents signal delivery to wrong processes
212
+ - Eliminates PID confusion attacks
213
+ - Provides capability-based process references
214
+ - Works with Linux security modules (SELinux, AppArmor)
215
+
216
+ ## Further Reading
217
+
218
+ - [Linux pidfd documentation](https://man7.org/linux/man-pages/man2/pidfd_open.2.html)
219
+ - [LWN: Process-descriptor file descriptors](https://lwn.net/Articles/801319/)
220
+ - [Bringing Linux pidfd to Ruby](https://mensfeld.github.io/bringing_linux_pidfd_to_ruby/)
221
+
222
+ ## Author
223
+
224
+ Based on the pidfd implementation in [Karafka](https://github.com/karafka/karafka) by Maciej Mensfeld.
225
+
226
+ ## Acknowledgments
227
+
228
+ Special thanks to:
229
+ - The Linux kernel team for implementing pidfd
230
+ - KJ Tsanaktsidis for Ruby core contributions discussions
231
+ - The Karafka community for battle-testing this implementation
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pidfd
4
+ # Namespace for all the pidfd errors
5
+ module Errors
6
+ # Base error class for all pidfd errors
7
+ BaseError = Class.new(StandardError)
8
+
9
+ # Error raised when pidfd_open syscall fails
10
+ PidfdOpenFailedError = Class.new(BaseError)
11
+
12
+ # Error raised when pidfd_signal syscall fails
13
+ PidfdSignalFailedError = Class.new(BaseError)
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pidfd
4
+ # Current version of the pidfd gem
5
+ VERSION = '0.6.0'
6
+ end
data/lib/pidfd.rb ADDED
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ # Main class that wraps Linux pidfd functionality for Ruby
6
+ class Pidfd
7
+ extend FFI::Library
8
+
9
+ begin
10
+ ffi_lib FFI::Library::LIBC
11
+
12
+ # direct usage of this is only available since glibc 2.36, hence we use bindings and call
13
+ # it directly via syscalls
14
+ attach_function :fdpid_open, :syscall, %i[long int uint], :int
15
+ attach_function :fdpid_signal, :syscall, %i[long int int pointer uint], :int
16
+ attach_function :waitid, %i[int int pointer uint], :int
17
+
18
+ API_SUPPORTED = true
19
+ # LoadError is a parent to FFI::NotFoundError
20
+ rescue LoadError
21
+ API_SUPPORTED = false
22
+ ensure
23
+ private_constant :API_SUPPORTED
24
+ end
25
+
26
+ # https://github.com/torvalds/linux/blob/7e90b5c295/include/uapi/linux/wait.h#L20
27
+ P_PIDFD = 3
28
+
29
+ # Wait for child processes that have exited
30
+ WEXITED = 4
31
+
32
+ # Default syscall numbers for x86_64 Linux
33
+ # These can be overridden if needed for different architectures
34
+ # Pidfd open call number
35
+ DEFAULT_PIDFD_OPEN_SYSCALL = 434
36
+
37
+ # Pidfd signal call number
38
+ DEFAULT_PIDFD_SIGNAL_SYSCALL = 424
39
+
40
+ private_constant :P_PIDFD, :WEXITED
41
+
42
+ class << self
43
+ # @return [Integer] syscall number for pidfd_open
44
+ attr_accessor :pidfd_open_syscall
45
+
46
+ # @return [Integer] syscall number for pidfd_signal
47
+ attr_accessor :pidfd_signal_syscall
48
+
49
+ # @return [Boolean] true if syscall is supported via FFI
50
+ def supported?
51
+ # If we were not even able to load the FFI C lib, it won't be supported
52
+ return false unless API_SUPPORTED
53
+ # Won't work on macOS because it does not support pidfd
54
+ return false if RUBY_DESCRIPTION.include?('darwin')
55
+ # Won't work on Windows for the same reason as on macOS
56
+ return false if RUBY_DESCRIPTION.match?(/mswin|ming|cygwin/)
57
+
58
+ # There are some OSes like BSD that will have C lib for FFI bindings but will not support
59
+ # the needed syscalls. In such cases, we can just try and fail, which will indicate it
60
+ # won't work. The same applies to using new glibc on an old kernel.
61
+ new(::Process.pid)
62
+
63
+ true
64
+ rescue Errors::PidfdOpenFailedError
65
+ false
66
+ end
67
+ end
68
+
69
+ # Set default syscall numbers
70
+ self.pidfd_open_syscall = DEFAULT_PIDFD_OPEN_SYSCALL
71
+ self.pidfd_signal_syscall = DEFAULT_PIDFD_SIGNAL_SYSCALL
72
+
73
+ # @param pid [Integer] pid of the node we want to work with
74
+ def initialize(pid)
75
+ @mutex = Mutex.new
76
+
77
+ @pid = pid
78
+ @pidfd = open_pidfd(pid)
79
+ @pidfd_io = IO.new(@pidfd)
80
+ end
81
+
82
+ # @return [Boolean] true if given process is alive, false if no longer
83
+ def alive?
84
+ @pidfd_select ||= [@pidfd_io]
85
+
86
+ if @mutex.owned?
87
+ return false if @cleaned
88
+
89
+ IO.select(@pidfd_select, nil, nil, 0).nil?
90
+ else
91
+ @mutex.synchronize do
92
+ return false if @cleaned
93
+
94
+ IO.select(@pidfd_select, nil, nil, 0).nil?
95
+ end
96
+ end
97
+ end
98
+
99
+ # Cleans the zombie process
100
+ # @note This should run **only** on processes that exited, otherwise will wait
101
+ def cleanup
102
+ @mutex.synchronize do
103
+ return if @cleaned
104
+
105
+ waitid(P_PIDFD, @pidfd, nil, WEXITED)
106
+
107
+ @pidfd_io.close
108
+ @pidfd_select = nil
109
+ @pidfd_io = nil
110
+ @pidfd = nil
111
+ @cleaned = true
112
+ end
113
+ end
114
+
115
+ # Sends given signal to the process using its pidfd
116
+ # @param sig_name [String] signal name
117
+ # @return [Boolean] true if signal was sent, otherwise false or error raised. `false`
118
+ # returned when we attempt to send a signal to a dead process
119
+ # @note It will not send signals to dead processes
120
+ def signal(sig_name)
121
+ @mutex.synchronize do
122
+ return false if @cleaned
123
+ # Never signal processes that are dead
124
+ return false unless alive?
125
+
126
+ result = fdpid_signal(
127
+ self.class.pidfd_signal_syscall,
128
+ @pidfd,
129
+ Signal.list.fetch(sig_name),
130
+ nil,
131
+ 0
132
+ )
133
+
134
+ return true if result.zero?
135
+
136
+ raise Errors::PidfdSignalFailedError, result
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ # Opens a pidfd for the provided pid
143
+ # @param pid [Integer]
144
+ # @return [Integer] pidfd
145
+ def open_pidfd(pid)
146
+ pidfd = fdpid_open(
147
+ self.class.pidfd_open_syscall,
148
+ pid,
149
+ 0
150
+ )
151
+
152
+ return pidfd if pidfd != -1
153
+
154
+ raise Errors::PidfdOpenFailedError, pidfd
155
+ end
156
+ end
157
+
158
+ require_relative 'pidfd/version'
159
+ require_relative 'pidfd/errors'
data/pidfd.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/pidfd/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'pidfd'
7
+ spec.version = Pidfd::VERSION
8
+ spec.authors = ['Maciej Mensfeld']
9
+ spec.email = ['maciej@mensfeld.pl']
10
+
11
+ spec.summary = 'Ruby wrapper for Linux pidfd system calls'
12
+ spec.homepage = 'https://github.com/mensfeld/pidfd'
13
+ spec.license = 'MIT'
14
+ spec.description = <<~DESC
15
+ Provides race-free process management using Linux pidfd (process file descriptors) for safer
16
+ signal delivery and process monitoring
17
+ DESC
18
+
19
+ spec.required_ruby_version = '>= 3.2.0'
20
+
21
+ spec.metadata['homepage_uri'] = spec.homepage
22
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
23
+ spec.metadata['source_code_uri'] = 'https://github.com/mensfeld/pidfd'
24
+ spec.metadata['changelog_uri'] = 'https://github.com/mensfeld/pidfd/blob/main/CHANGELOG.md'
25
+ spec.metadata['documentation_uri'] = 'https://github.com/mensfeld/pidfd#readme'
26
+ spec.metadata['rubygems_mfa_required'] = 'true'
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ spec.files = Dir.glob('{lib}/**/*') + %w[LICENSE CHANGELOG.md README.md pidfd.gemspec]
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_dependency 'ffi', '>= 1.15'
33
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pidfd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Maciej Mensfeld
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ description: |
27
+ Provides race-free process management using Linux pidfd (process file descriptors) for safer
28
+ signal delivery and process monitoring
29
+ email:
30
+ - maciej@mensfeld.pl
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - CHANGELOG.md
36
+ - LICENSE
37
+ - README.md
38
+ - lib/pidfd.rb
39
+ - lib/pidfd/errors.rb
40
+ - lib/pidfd/version.rb
41
+ - pidfd.gemspec
42
+ homepage: https://github.com/mensfeld/pidfd
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/mensfeld/pidfd
47
+ allowed_push_host: https://rubygems.org
48
+ source_code_uri: https://github.com/mensfeld/pidfd
49
+ changelog_uri: https://github.com/mensfeld/pidfd/blob/main/CHANGELOG.md
50
+ documentation_uri: https://github.com/mensfeld/pidfd#readme
51
+ rubygems_mfa_required: 'true'
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 3.2.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.7.0
67
+ specification_version: 4
68
+ summary: Ruby wrapper for Linux pidfd system calls
69
+ test_files: []