Files
containerization/vminitd/Sources/VminitdCore/OSFile+Splice.swift
T

125 lines
5.9 KiB
Swift
Raw Normal View History

//===----------------------------------------------------------------------===//
// 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