2026-06-21 20:22:21 -07:00
|
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
|
// 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 {
|
2026-07-28 15:05:37 -07:00
|
|
|
/// [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
|
2026-06-21 20:22:21 -07:00
|
|
|
|
2026-07-28 15:05:37 -07:00
|
|
|
/// Bytes read from the source that the destination hasn't accepted yet.
|
|
|
|
|
var pendingBytes: Int { bytesIn - bytesOut }
|
2026-06-21 20:22:21 -07:00
|
|
|
|
2026-07-28 15:05:37 -07:00
|
|
|
fileprivate var pipeReader: Int32 { pipe.fileHandleForReading.fileDescriptor }
|
|
|
|
|
fileprivate var pipeWriter: Int32 { pipe.fileHandleForWriting.fileDescriptor }
|
2026-06-21 20:22:21 -07:00
|
|
|
|
2026-07-28 15:05:37 -07:00
|
|
|
init(from: Int32, to: Int32) {
|
|
|
|
|
self.from = from
|
|
|
|
|
self.to = to
|
2026-06-21 20:22:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 15:05:37 -07:00
|
|
|
/// 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
|
|
|
|
|
}
|
2026-06-21 20:22:21 -07:00
|
|
|
|
2026-07-28 15:05:37 -07:00
|
|
|
/// 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)
|
2026-06-21 20:22:21 -07:00
|
|
|
while true {
|
2026-07-28 15:05:37 -07:00
|
|
|
// 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
|
2026-06-21 20:22:21 -07:00
|
|
|
}
|
2026-07-28 15:05:37 -07:00
|
|
|
if n == 0 {
|
|
|
|
|
direction.sawSourceEOF = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
direction.bytesIn += n
|
|
|
|
|
if n < toRead { break }
|
2026-06-21 20:22:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-28 15:05:37 -07:00
|
|
|
// 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 {
|
2026-06-21 20:22:21 -07:00
|
|
|
if errno != EAGAIN && errno != EIO {
|
|
|
|
|
throw POSIXError(.init(rawValue: errno)!)
|
|
|
|
|
}
|
2026-07-28 15:05:37 -07:00
|
|
|
// 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
|
2026-06-21 20:22:21 -07:00
|
|
|
}
|
2026-07-28 15:05:37 -07:00
|
|
|
if n == 0 {
|
|
|
|
|
return .brokenPipe
|
2026-06-21 20:22:21 -07:00
|
|
|
}
|
2026-07-28 15:05:37 -07:00
|
|
|
direction.bytesOut += n
|
2026-06-21 20:22:21 -07:00
|
|
|
}
|
2026-07-28 15:05:37 -07:00
|
|
|
// 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.
|
2026-06-21 20:22:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|