//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the Containerization project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// #if os(Linux) import Foundation import LCShim extension OSFile { /// [Nucleic vendored patch] One direction of a bidirectional relay: `from` fd → transfer /// pipe → `to` fd. Each direction owns its OWN pipe and byte counters. /// /// The previous `SpliceFile` design threaded ONE offset pair through BOTH directions of /// `VsockProxy`'s relay (a fd's struct served as read-counter in one direction and /// write-counter in the other). That was survivable only while every splice call fully /// drained its pipe before returning. Once the EAGAIN-return backpressure patch let pending /// bytes persist across calls, one parked direction skewed the shared counters for the /// other: its write leg's `to.offset < from.offset` guard went false with data still in the /// pipe, and the outer loop then alternated read-EAGAIN/skip-write forever — a hard spin on /// vminitd's single ProcessSupervisor poller thread that froze every exec's stdio and every /// control-plane relay in the container until the VM was recreated (the persistent /// "produced no output within 60s" / dead-control-plane state). struct RelayDirection: Sendable { let from: Int32 let to: Int32 private let pipe = Pipe() /// Bytes spliced from `from` into the transfer pipe so far. fileprivate var bytesIn = 0 /// Bytes spliced from the transfer pipe into `to` so far. fileprivate var bytesOut = 0 /// The source reported EOF. The direction only FINISHES (`.eof`) once the pipe has /// also drained, so the stream's tail is never dropped by an early SHUT_WR. fileprivate var sawSourceEOF = false /// Bytes read from the source that the destination hasn't accepted yet. var pendingBytes: Int { bytesIn - bytesOut } fileprivate var pipeReader: Int32 { pipe.fileHandleForReading.fileDescriptor } fileprivate var pipeWriter: Int32 { pipe.fileHandleForWriting.fileDescriptor } init(from: Int32, to: Int32) { self.from = from self.to = to } } /// The terminal state of one `relay` pass over a direction. enum RelayResult: Sendable { /// No more progress possible right now: the source has no data (EAGAIN) or the /// destination is full (its EPOLLOUT edge resumes the flush of `pendingBytes`). case idle /// Source EOF observed AND the pipe fully drained — the direction is complete; the /// caller should SHUT_WR the destination. case eof /// The destination hung up mid-write; nothing further can be delivered. case brokenPipe } /// Move as much data as possible along `direction` without blocking. `count` bounds the /// bytes buffered in the transfer pipe (it matches the default pipe capacity). static func relay(_ direction: inout RelayDirection, count: Int = 1 << 16) throws -> RelayResult { let flags = UInt32(bitPattern: LCShim.SPLICE_F_MOVE | LCShim.SPLICE_F_NONBLOCK) while true { // Read leg: source → pipe, until the pipe is full, the source runs dry, or EOF. var sourceDry = false if !direction.sawSourceEOF { while direction.pendingBytes < count { let toRead = count - direction.pendingBytes let n = LCShim.splice(direction.from, nil, direction.pipeWriter, nil, toRead, flags) if n == -1 { if errno != EAGAIN && errno != EIO { throw POSIXError(.init(rawValue: errno)!) } sourceDry = true break } if n == 0 { direction.sawSourceEOF = true break } direction.bytesIn += n if n < toRead { break } } } // Write leg: pipe → destination, until drained or the destination pushes back. while direction.pendingBytes > 0 { let n = LCShim.splice(direction.pipeReader, nil, direction.to, nil, direction.pendingBytes, flags) if n == -1 { if errno != EAGAIN && errno != EIO { throw POSIXError(.init(rawValue: errno)!) } // Destination full: park with the remainder in the pipe. The destination // fd's EPOLLOUT edge re-enters and resumes exactly here — never spin, and // never block the shared poller thread. return .idle } if n == 0 { return .brokenPipe } direction.bytesOut += n } // Pipe is drained here. if direction.sawSourceEOF { return .eof } if sourceDry { return .idle } // The read leg stopped only because the pipe filled (or read a full window): // go around again — the source may still have data. } } } #endif