Class: OsCtld::Container::NfsCancellation
- Inherits:
-
Object
- Object
- OsCtld::Container::NfsCancellation
- Includes:
- OsCtl::Lib::Utils::Log
- Defined in:
- lib/osctld/container/nfs_cancellation.rb
Overview
Namespace handles belong to one container run, not a reusable PID. Never follow tenant mount paths or use the tenant's potentially replaced /sys.
Defined Under Namespace
Classes: WorkerTimeout
Constant Summary collapse
- NS_GET_USERNS =
0xb701- NS_GET_PARENT =
0xb702- PF_EXITING =
0x00000004- WORKER_TIMEOUT =
30- MAX_WORKER_OUTPUT =
65_536
Instance Method Summary collapse
-
#abort ⇒ Object
The caller must quiesce tenant work before requesting terminal abort.
-
#abort_if_exiting ⇒ Object
PID1 can block in exit_files(), before LXC emits STOPPING.
- #abort_locked ⇒ Object protected
- #cancel_filesystems(path) ⇒ Object protected
- #cancel_in_worker ⇒ Object protected
- #cancel_namespace(path, subtree: false, deadline: monotonic_time + WORKER_TIMEOUT) ⇒ Object protected
- #cancel_namespaces(deadline) ⇒ Object protected
- #capture(pid) ⇒ Object
- #capture_descendants ⇒ Object protected
- #capture_netns(path) ⇒ Object protected
- #close ⇒ Object
- #init_exiting?(path) ⇒ Boolean protected
-
#initialize(ct, proc_root: '/proc') ⇒ NfsCancellation
constructor
A new instance of NfsCancellation.
- #log_type ⇒ Object
- #member?(path) ⇒ Boolean protected
- #monotonic_time ⇒ Object protected
- #owned_network_namespace?(netns) ⇒ Boolean protected
- #read_worker_output(reader, deadline) ⇒ Object protected
- #retain_init(path, dir) ⇒ Object protected
- #root_network_namespace?(netns) ⇒ Boolean protected
- #same_namespace?(io, stat) ⇒ Boolean protected
- #terminate_worker(pid) ⇒ Object protected
- #wait_for_worker(pid, deadline) ⇒ Object protected
- #with_process(pid) ⇒ Object protected
- #worker_time_left(deadline) ⇒ Object protected
- #write_shutdown_control(control, deadline) ⇒ Object protected
Constructor Details
#initialize(ct, proc_root: '/proc') ⇒ NfsCancellation
Returns a new instance of NfsCancellation.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 21 def initialize(ct, proc_root: '/proc') # Run identity is immutable. Never acquire the container lock while # holding @mutex: Container#stopped closes this object under that lock. @ident = ct.ident.dup.freeze @payload = File.join('/', ct.cgroup_path, "lxc.payload.#{ct.id}").freeze @payload_prefix = "#{@payload}/".freeze @proc_root = proc_root @mutex = Mutex.new @owner = nil @netns = {} @init_proc = nil @exit_cancelled = false @closed = false end |
Instance Method Details
#abort ⇒ Object
The caller must quiesce tenant work before requesting terminal abort. Retained handles remain usable even after init's /proc entry disappears.
62 63 64 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 62 def abort @mutex.synchronize { abort_locked } end |
#abort_if_exiting ⇒ Object
PID1 can block in exit_files(), before LXC emits STOPPING. A retained proc directory identifies the original task even if its PID is reused.
68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 68 def abort_if_exiting @mutex.synchronize do return 0 if @exit_cancelled || !@init_proc path = File.join(@proc_root, 'self/fd', @init_proc.fileno.to_s) return 0 unless init_exiting?(path) count = abort_locked @exit_cancelled = true count end end |
#abort_locked ⇒ Object (protected)
105 106 107 108 109 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 105 def abort_locked return 0 unless @owner cancel_in_worker end |
#cancel_filesystems(path) ⇒ Object (protected)
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 338 def cancel_filesystems(path) return 0 unless Dir.exist?(path) count = 0 Dir.children(path).each do |name| next unless /\A(?:[0-9]+:[0-9]+|server-[0-9]+)\z/.match?(name) begin File.open(File.join(path, name, 'shutdown'), File::WRONLY) { |f| f.write("1\n") } count += 1 rescue Errno::ENOENT next end end count end |
#cancel_in_worker ⇒ Object (protected)
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 200 def cancel_in_worker deadline = monotonic_time + WORKER_TIMEOUT reader, writer = IO.pipe pid = SwitchUser.fork(keep_fds: [writer, @owner, *@netns.values].compact) do Process.setproctitle("osctld: #{@ident} NFS cancellation") # Bound the process/thread scan with the same deadline as sysfs work. # Newly discovered handles need live only until this worker exits. capture_descendants writer.puts(JSON.generate(count: cancel_namespaces(deadline))) exit!(true) rescue StandardError => e writer.puts(JSON.generate(error: "#{e.class}: #{e.}")) exit! end writer.close output = read_worker_output(reader, deadline) status = wait_for_worker(pid, deadline) pid = nil result = JSON.parse(output, symbolize_names: true) raise "NFS cancellation failed: #{result[:error]}" unless status.success? result.fetch(:count) ensure reader&.close writer&.close unless writer&.closed? terminate_worker(pid) if pid end |
#cancel_namespace(path, subtree: false, deadline: monotonic_time + WORKER_TIMEOUT) ⇒ Object (protected)
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 304 def cancel_namespace(path, subtree: false, deadline: monotonic_time + WORKER_TIMEOUT) return 0 unless Dir.exist?(path) tree_control = File.join(path, 'net/nfs_client/shutdown_tree') if subtree && File.exist?(tree_control) # Only select the authenticated run owner, never an ancestor reached # through an inherited host namespace or a tenant-controlled path. write_shutdown_control(tree_control, deadline) return 1 end control = File.join(path, 'net/nfs_client/shutdown') if File.exist?(control) # Sticky shutdown covers initializing mounts and future RPC clients. write_shutdown_control(control, deadline) return 1 end # Older kernels still enforce soft mounts and lack the admission barrier. cancel_filesystems(path) end |
#cancel_namespaces(deadline) ⇒ Object (protected)
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 277 def cancel_namespaces(deadline) sys = OsCtl::Lib::Sys.new sys.unshare_ns(OsCtl::Lib::Sys::CLONE_NEWNS) sys.make_rslave('/') count = 0 Dir.mktmpdir('nfs-cancel-', '/run/osctl') do |mountpoint| @netns.each_value do |netns| sys.setns_io(netns, OsCtl::Lib::Sys::CLONE_NEWNET) sys.mount_sysfs(mountpoint) begin count += cancel_namespace(File.join(mountpoint, 'fs/nfs'), subtree: root_network_namespace?(netns), deadline: deadline) ensure sys.unmount(mountpoint) end end end count end |
#capture(pid) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 36 def capture(pid) return if pid.nil? @mutex.synchronize do return if @closed with_process(pid) do |path, dir| next unless member?(path) File.open(File.join(path, 'ns/user')) do |userns| next if same_namespace?(userns, File.stat(File.join(@proc_root, 'self/ns/user'))) next if @owner && !same_namespace?(userns, @owner.stat) @owner ||= userns.dup capture_netns(path) retain_init(path, dir) end end end nil rescue Errno::ENOENT, Errno::ESRCH nil end |
#capture_descendants ⇒ Object (protected)
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 177 def capture_descendants Dir.foreach(@proc_root) do |entry| next unless /\A[0-9]+\z/.match?(entry) begin with_process(entry) do |path| # setns() is per-thread. The group leader need not use the # network namespace containing another thread's NFS client. Dir.children(File.join(path, 'task')).each do |tid| thread_path = File.join(path, 'task', tid) begin capture_netns(thread_path) if member?(thread_path) rescue Errno::ENOENT, Errno::ESRCH next end end end rescue Errno::ENOENT, Errno::ESRCH next end end end |
#capture_netns(path) ⇒ Object (protected)
168 169 170 171 172 173 174 175 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 168 def capture_netns(path) File.open(File.join(path, 'ns/net')) do |netns| next unless owned_network_namespace?(netns) stat = netns.stat @netns[[stat.dev, stat.ino]] ||= netns.dup end end |
#close ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 81 def close @mutex.synchronize do @closed = true @netns.each_value(&:close) @netns.clear @owner&.close @owner = nil @init_proc&.close @init_proc = nil end end |
#init_exiting?(path) ⇒ Boolean (protected)
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 120 def init_exiting?(path) # The group leader can call pthread_exit while another init thread # keeps the namespace alive. Require every remaining thread to exit. Dir.children(File.join(path, 'task')).all? do |tid| stat = File.read(File.join(path, 'task', tid, 'stat')) fields = stat[(stat.rindex(')') + 1)..].split # stat field 9 is flags; the fields array starts at field 3 (state). Integer(fields.fetch(6)).anybits?(PF_EXITING) rescue Errno::ENOENT, Errno::ESRCH true end rescue Errno::ENOENT, Errno::ESRCH # Init vanished before we sampled PF_EXITING; its retained namespaces # may still be held by the LXC monitor or detached mounts. true end |
#log_type ⇒ Object
93 94 95 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 93 def log_type "nfs-cancel=#{@ident}" end |
#member?(path) ⇒ Boolean (protected)
137 138 139 140 141 142 143 144 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 137 def member?(path) File.foreach(File.join(path, 'cgroup')).any? do |line| _id, controllers, cgroup = line.strip.split(':', 3) next false unless controllers == '' || controllers&.split(',')&.include?('freezer') cgroup == @payload || cgroup&.start_with?(@payload_prefix) end end |
#monotonic_time ⇒ Object (protected)
228 229 230 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 228 def monotonic_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
#owned_network_namespace?(netns) ⇒ Boolean (protected)
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 151 def owned_network_namespace?(netns) owner = IO.for_fd(netns.ioctl(NS_GET_USERNS)) begin loop do return true if same_namespace?(owner, @owner.stat) parent = IO.for_fd(owner.ioctl(NS_GET_PARENT)) owner.close owner = parent end rescue Errno::EPERM false ensure owner.close end end |
#read_worker_output(reader, deadline) ⇒ Object (protected)
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 239 def read_worker_output(reader, deadline) output = +'' loop do left = worker_time_left(deadline) chunk = reader.read_nonblock(4096, exception: false) case chunk when nil return output when :wait_readable reader.wait_readable(left) else output << chunk raise 'NFS cancellation worker output is too large' if output.bytesize > MAX_WORKER_OUTPUT end end end |
#retain_init(path, dir) ⇒ Object (protected)
111 112 113 114 115 116 117 118 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 111 def retain_init(path, dir) return if @init_proc nspid = File.foreach(File.join(path, 'status')).find { |line| line.start_with?('NSpid:') } return unless nspid&.split&.last == '1' @init_proc = dir.dup end |
#root_network_namespace?(netns) ⇒ Boolean (protected)
297 298 299 300 301 302 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 297 def root_network_namespace?(netns) owner = IO.for_fd(netns.ioctl(NS_GET_USERNS)) same_namespace?(owner, @owner.stat) ensure owner&.close end |
#same_namespace?(io, stat) ⇒ Boolean (protected)
146 147 148 149 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 146 def same_namespace?(io, stat) actual = io.stat actual.dev == stat.dev && actual.ino == stat.ino end |
#terminate_worker(pid) ⇒ Object (protected)
265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 265 def terminate_worker(pid) # An unreaped child keeps its PID reserved. Never signal a reaped PID. return if Process.wait2(pid, Process::WNOHANG) Process.kill('KILL', pid) # A broken kernel path might remain uninterruptible. Reap asynchronously # rather than turn the error/timeout path into another unbounded wait. Process.detach(pid) rescue Errno::ECHILD nil end |
#wait_for_worker(pid, deadline) ⇒ Object (protected)
256 257 258 259 260 261 262 263 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 256 def wait_for_worker(pid, deadline) loop do result = Process.wait2(pid, Process::WNOHANG) return result.last if result sleep([worker_time_left(deadline), 0.05].min) end end |
#with_process(pid) ⇒ Object (protected)
99 100 101 102 103 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 99 def with_process(pid) File.open(File.join(@proc_root, pid.to_s)) do |dir| yield File.join(@proc_root, 'self/fd', dir.fileno.to_s), dir end end |
#worker_time_left(deadline) ⇒ Object (protected)
232 233 234 235 236 237 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 232 def worker_time_left(deadline) left = deadline - monotonic_time raise WorkerTimeout, 'NFS cancellation worker timed out' unless left > 0 left end |
#write_shutdown_control(control, deadline) ⇒ Object (protected)
326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/osctld/container/nfs_cancellation.rb', line 326 def write_shutdown_control(control, deadline) loop do File.open(control, File::WRONLY) { |f| f.write("1\n") } return rescue Errno::EBUSY # Kernel-side subtree admission uses a trylock to avoid blocking a # concurrent netns teardown. Retry here, outside the sysfs callback, # within the existing bounded cancellation-worker deadline. sleep([worker_time_left(deadline), 0.05].min) end end |