Class: OsCtld::ProcessIdentity

Inherits:
Object
  • Object
show all
Defined in:
lib/osctld/process_identity.rb

Overview

Stable process identity with descriptors opened through one /proc entry.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pid, namespaces: [], root: false) ⇒ ProcessIdentity

Returns a new instance of ProcessIdentity.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/osctld/process_identity.rb', line 19

def initialize(pid, namespaces: [], root: false)
  @pid = Integer(pid)
  raise ArgumentError, 'pid has to be positive' if @pid <= 0

  @sys = OsCtl::Lib::Sys.new
  @pidfd = @sys.pidfd_open(@pid)
  @pidfd_stat = @pidfd.stat
  @namespaces = {}
  @proc_dir = nil
  @proc_dir_stat = nil
  @root_dir = nil

  @proc_dir = File.open(File.join('/proc', @pid.to_s))
  @proc_dir_stat = @proc_dir.stat

  unless @proc_dir_stat.directory? && alive?
    raise Errno::ESRCH, "process #{@pid} exited"
  end

  @root_dir = open_proc_entry('root') if root

  namespaces.each do |name|
    ns_name = name.to_sym
    @namespaces[ns_name] = open_proc_entry(File.join('ns', ns_name.to_s))
  end

  raise Errno::ESRCH, "process #{@pid} exited" unless alive?
rescue StandardError
  close
  raise
end

Instance Attribute Details

#pid ⇒ Object (readonly)

Returns the value of attribute pid.



6
7
8
# File 'lib/osctld/process_identity.rb', line 6

def pid
  @pid
end

#pidfd ⇒ Object (readonly)

Returns the value of attribute pidfd.



6
7
8
# File 'lib/osctld/process_identity.rb', line 6

def pidfd
  @pidfd
end

#proc_dir ⇒ Object (readonly)

Returns the value of attribute proc_dir.



6
7
8
# File 'lib/osctld/process_identity.rb', line 6

def proc_dir
  @proc_dir
end

#root_dir ⇒ Object (readonly)

Returns the value of attribute root_dir.



6
7
8
# File 'lib/osctld/process_identity.rb', line 6

def root_dir
  @root_dir
end

Class Method Details

.open(pid, namespaces: [], root: false) ⇒ Object



8
9
10
11
12
13
14
15
16
17
# File 'lib/osctld/process_identity.rb', line 8

def self.open(pid, namespaces: [], root: false)
  identity = new(pid, namespaces:, root:)
  return identity unless block_given?

  begin
    yield identity
  ensure
    identity.close
  end
end

Instance Method Details

#alive? ⇒ Boolean

Returns:

  • (Boolean)


51
52
53
54
55
56
57
58
# File 'lib/osctld/process_identity.rb', line 51

def alive?
  fd = pidfd
  return false if fd.nil? || fd.closed?

  @sys.pidfd_alive?(fd)
rescue SystemCallError, IOError
  false
end

#authenticate!(cgroup_path:) ⇒ Object

Recheck that every retained descriptor still describes this live process and that the process remains below the expected cgroup. This is used at the last responsible moment before namespace or mount effects.



72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/osctld/process_identity.rb', line 72

def authenticate!(cgroup_path:)
  verify_process_descriptors!

  verify_root_context! if root_dir || @namespaces.any?
  unless in_cgroup_subtree?(cgroup_path)
    raise Errno::EXDEV, "process #{pid} left cgroup #{cgroup_path}"
  end

  verify_process_descriptors!

  self
end

#cgroup_entries ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/osctld/process_identity.rb', line 89

def cgroup_entries
  read_proc_file('cgroup').lines.map do |line|
    hierarchy, controllers, path = line.strip.split(':', 3)
    if hierarchy.nil? || controllers.nil? || path.nil? || path.empty?
      raise ArgumentError, "malformed cgroup entry #{line.inspect}"
    end

    {
      hierarchy:,
      controllers: controllers.split(','),
      path:
    }
  end
end

#cgroup_paths ⇒ Object



104
105
106
# File 'lib/osctld/process_identity.rb', line 104

def cgroup_paths
  cgroup_entries.map { |entry| entry.fetch(:path) }
end

#close ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/osctld/process_identity.rb', line 139

def close
  @namespaces&.each_value { |io| close_io(io) }
  close_io(@root_dir)
  close_io(@proc_dir)
  close_io(@pidfd)
  @namespaces = {}
  @root_dir = nil
  @proc_dir = nil
  @proc_dir_stat = nil
  @pidfd = nil
  @pidfd_stat = nil
end

#close_io(io) ⇒ Object (protected)



255
256
257
# File 'lib/osctld/process_identity.rb', line 255

def close_io(io)
  io&.close unless io&.closed?
end

#duplicate ⇒ Object

Duplicate every held descriptor without resolving the numeric PID again. The copy can outlive this identity and remains bound to the same process, proc entry, root, and namespaces.



63
64
65
66
67
# File 'lib/osctld/process_identity.rb', line 63

def duplicate
  copy = self.class.allocate
  copy.send(:initialize_duplicate, self)
  copy
end

#files ⇒ Object



135
136
137
# File 'lib/osctld/process_identity.rb', line 135

def files
  [pidfd, proc_dir, root_dir, *@namespaces.values].compact
end

#in_cgroup_subtree?(path) ⇒ Boolean

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
116
117
118
# File 'lib/osctld/process_identity.rb', line 108

def in_cgroup_subtree?(path)
  prefix = File.absolute_path(path, '/')

  entries = cgroup_entries
  return false if entries.empty?

  entries.all? do |entry|
    current = File.absolute_path(entry.fetch(:path), '/')
    current == prefix || current.start_with?("#{prefix}/")
  end
end

#initialize_duplicate(source) ⇒ Object (protected)



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/osctld/process_identity.rb', line 154

def initialize_duplicate(source)
  @pid = source.pid
  @sys = source.instance_variable_get(:@sys)
  @pidfd = nil
  @pidfd_stat = source.instance_variable_get(:@pidfd_stat)
  @proc_dir = nil
  @proc_dir_stat = source.instance_variable_get(:@proc_dir_stat)
  @root_dir = nil
  @namespaces = {}

  raise Errno::ESRCH, "process #{@pid} exited" unless source.alive?

  @pidfd = source.pidfd.dup
  @proc_dir = source.proc_dir.dup
  @root_dir = source.root_dir ? source.root_dir.dup : nil
  source.instance_variable_get(:@namespaces).each do |name, io|
    @namespaces[name] = io.dup
  end

  raise Errno::ESRCH, "process #{@pid} exited" unless alive?
rescue StandardError
  close
  raise
end

#namespace(name) ⇒ Object



85
86
87
# File 'lib/osctld/process_identity.rb', line 85

def namespace(name)
  @namespaces.fetch(name.to_sym)
end

#open_proc_entry(name) ⇒ Object (protected)



219
220
221
222
223
224
225
226
227
# File 'lib/osctld/process_identity.rb', line 219

def open_proc_entry(name, **)
  @sys.openat_io(proc_dir, name, **)
rescue SystemCallError, IOError => e
  raise Errno::ESRCH, "process #{pid} exited", e.backtrace unless alive?

  raise e.class,
        "unable to open /proc/#{pid}/#{name} through retained proc descriptor: #{e.message}",
        e.backtrace
end

#open_proc_file(name, mode = 'r') ⇒ Object



120
121
122
123
124
125
126
127
128
129
# File 'lib/osctld/process_identity.rb', line 120

def open_proc_file(name, mode = 'r', &)
  io = open_proc_entry(name, flags: proc_open_flags(mode))
  return io unless block_given?

  begin
    yield io
  ensure
    io.close
  end
end

#proc_open_flags(mode) ⇒ Object (protected)



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/osctld/process_identity.rb', line 236

def proc_open_flags(mode)
  case mode
  when 'r'
    File::RDONLY
  when 'w'
    File::WRONLY | File::TRUNC
  when 'a'
    File::WRONLY | File::APPEND
  when 'r+'
    File::RDWR
  when 'w+'
    File::RDWR | File::TRUNC
  when 'a+'
    File::RDWR | File::APPEND
  else
    Integer(mode)
  end
end

#read_proc_file(name) ⇒ Object



131
132
133
# File 'lib/osctld/process_identity.rb', line 131

def read_proc_file(name)
  open_proc_file(name, &:read)
end

#same_file?(left, right) ⇒ Boolean (protected)

Returns:

  • (Boolean)


229
230
231
232
233
234
# File 'lib/osctld/process_identity.rb', line 229

def same_file?(left, right)
  left_stat = left.respond_to?(:stat) ? left.stat : left
  right_stat = right.respond_to?(:stat) ? right.stat : right

  left_stat.dev == right_stat.dev && left_stat.ino == right_stat.ino
end

#verify_process_descriptors! ⇒ Object (protected)

Raises:

  • (Errno::ESRCH)


205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/osctld/process_identity.rb', line 205

def verify_process_descriptors!
  raise Errno::ESRCH, "process #{pid} exited" unless alive?

  unless same_file?(pidfd, @pidfd_stat)
    raise Errno::ESTALE, "process #{pid} pidfd was substituted"
  end

  unless same_file?(proc_dir, @proc_dir_stat)
    raise Errno::ESTALE, "process #{pid} proc descriptor was substituted"
  end

  raise Errno::ESRCH, "process #{pid} exited" unless alive?
end

#verify_root_context! ⇒ Object (protected)



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/osctld/process_identity.rb', line 179

def verify_root_context!
  verify_process_descriptors!

  if root_dir
    current_root = open_proc_entry('root')
    unless same_file?(current_root, root_dir)
      raise Errno::ESTALE, "process #{pid} changed root"
    end
  end

  current_namespaces = []
  @namespaces.each do |name, held|
    current = open_proc_entry(File.join('ns', name.to_s))
    current_namespaces << current
    unless same_file?(current, held)
      display_name = name == :mnt ? 'mount' : name
      raise Errno::ESTALE, "process #{pid} changed #{display_name} namespace"
    end
  end

  verify_process_descriptors!
ensure
  current_namespaces&.each(&:close)
  current_root&.close
end