Merge nucleic/hazy-opal-yak-cjc0 into dev

This commit is contained in:
2026-07-17 23:48:59 -07:00
parent 608e7d0450
commit 65623f1c34
5 changed files with 403 additions and 202 deletions
+34 -2
View File
@@ -205,6 +205,36 @@ rebuild whenever a guest patch changes. Built locally, not in CI: the host frame
into a never-accepted backlog. A failed pre-relay connection is also closed explicitly.
Marked `[Nucleic vendored patch]`.
14. **Non-blocking, non-spinning guest I/O plane (`IOPair.swift`, `OSFile+Splice.swift`,
`VsockProxy.swift`) — the root cause of the "control plane unresponsive / all sessions stall"
wedge.** Every exec's stdio relay (`IOPair`) and every control-plane relay connection
(`VsockProxy`) share ONE thread: `ProcessSupervisor.default`'s epoll poller. Three defects let
a single slow peer freeze that thread — and with it every session's stdio AND the whole
container's control plane at once (probes then read "accepts connections but never answers";
before the manager-side recovery rework the host restarted the container over this, SIGKILLing
every session):
- **`IOPair` blocking write:** only *registered* fds get `O_NONBLOCK` (set by `Epoll.add`), and
the relay registers only its READ fd — the write fd (e.g. the vsock socket carrying an exec's
stdout to the host) stayed blocking. One slow-drained stream parked the poller thread in
`write(2)`. The relay now sets the write fd non-blocking up front and implements real
backpressure: a full destination stashes the remainder in a `pending` backlog, registers the
write fd for EPOLLOUT, and suspends reads until the flush completes (throttling the producing
process via its own pipe, not the shared thread). The old close-on-short-write path — dead
while the fd was blocking, live and stdio-dropping once non-blocking — is subsumed by the
backlog; genuine write errors still close the pair.
- **`OSFile.splice` busy-spin:** when the destination was full (EAGAIN) and the source idle,
the outer loop had no exit — it spun the poller thread at 100% until the peer drained (or
forever if it never did). The flush leg's EAGAIN branch now returns partial progress; the
un-flushed bytes stay in the transfer pipe and the destination's EPOLLOUT edge resumes the
flush (both `VsockProxy` handlers already pump both directions on both events).
- **`VsockProxy` registration race:** the client fd's epoll handler can fire — and splice
toward the server fd — before the second registration makes that fd non-blocking; a full
destination made that a genuinely blocking splice on the poller thread. Both fds are now set
non-blocking before either is registered.
All three are guest-side and INERT until the initfs image is rebuilt (`make vminit-image`, tag
`0.34.0-nucleic4`) and `ContainerEngine.vminitReference` is bumped after runtime validation.
Marked `[Nucleic vendored patch]`.
## Re-vendoring a newer upstream commit
1. `git clone` upstream (or copy `.build/checkouts/containerization` after bumping the URL pin
@@ -222,9 +252,11 @@ rebuild whenever a guest patch changes. Built locally, not in CI: the host frame
`Cgroup2Manager.swift` / `ManagedContainer.swift` / `ManagedProcess.swift`, patch #10
(`Socket.acceptStream` transient-error tolerance + `isTransientAcceptError`), patch #11 (the
`UnixSocketRelay` per-connection containment + fail-fast closes), patch #12 (the `VsockProxy`
cleanup/`try!`/listener hardening in `vminitd/`), and patch #13 (the prompt-free
cleanup/`try!`/listener hardening in `vminitd/`), patch #13 (the prompt-free
`KeychainQuery` reads: `withoutInteractiveUI` + the `errSecInteractionNotAllowed` handling +
the `save` duplicate retry). After re-applying any
the `save` duplicate retry), and patch #14 (the non-blocking/non-spinning guest I/O plane:
`IOPair` backpressure, the `OSFile.splice` EAGAIN return, and the `VsockProxy` pre-registration
non-blocking fds — all in `vminitd/`). After re-applying any
`vminitd/` patch, rebuild + publish the custom init image
with `make vminit-image` + `make vminit-image-push`, and bump `ContainerEngine.vminitReference`.
5. Update the commit hash above and in the root `Package.swift` comment.
+174 -165
View File
@@ -1,249 +1,258 @@
{
"originHash" : "6ccceb47b6a402e9ac07d23204ec7f4792823b22b96275cd67f8531787a60c04",
"pins" : [
"originHash": "264b211a5ea74fa24ced86faade5901700722c925484a26379cc4a6b40083c6c",
"pins": [
{
"identity" : "async-http-client",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client.git",
"state" : {
"revision" : "4b99975677236d13f0754339864e5360142ff5a1",
"version" : "1.30.3"
"identity": "async-http-client",
"kind": "remoteSourceControl",
"location": "https://github.com/swift-server/async-http-client.git",
"state": {
"revision": "4603a8036d921ea999fadb742931546c341f4bd7",
"version": "1.35.0"
}
},
{
"identity" : "grpc-swift-2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift-2.git",
"state" : {
"revision" : "f28854bc760a116e053fdfc4a48a9428c34625c0",
"version" : "2.3.0"
"identity": "grpc-swift-2",
"kind": "remoteSourceControl",
"location": "https://github.com/grpc/grpc-swift-2.git",
"state": {
"revision": "28cdd63ef88583ddc67d7bb179eab46fab465ce9",
"version": "2.4.2"
}
},
{
"identity" : "grpc-swift-nio-transport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift-nio-transport.git",
"state" : {
"revision" : "f37e0c2d293cea668b11e10e1fb1c24cb40781ff",
"version" : "2.4.4"
"identity": "grpc-swift-nio-transport",
"kind": "remoteSourceControl",
"location": "https://github.com/grpc/grpc-swift-nio-transport.git",
"state": {
"revision": "2ca31f06658ed288a2560e23ad649acbb3d6b3a3",
"version": "2.9.0"
}
},
{
"identity" : "grpc-swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift-protobuf.git",
"state" : {
"revision" : "19153231a03c2fda1f4ea60da1b92a2cb9c011d8",
"version" : "2.2.0"
"identity": "grpc-swift-protobuf",
"kind": "remoteSourceControl",
"location": "https://github.com/grpc/grpc-swift-protobuf.git",
"state": {
"revision": "176c5a434fd76f6f479848d1a8f7d44967534168",
"version": "2.4.1"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-algorithms.git",
"state" : {
"revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023",
"version" : "1.2.1"
"identity": "swift-algorithms",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-algorithms.git",
"state": {
"revision": "87e50f483c54e6efd60e885f7f5aa946cee68023",
"version": "1.2.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615",
"version" : "1.7.0"
"identity": "swift-argument-parser",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-argument-parser.git",
"state": {
"revision": "6a52f3251125d74daf04fcbd5e6f08a75d074382",
"version": "1.8.2"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "a54383ada6cecde007d374f58f864e29370ba5c3",
"version" : "1.3.2"
"identity": "swift-asn1",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-asn1.git",
"state": {
"revision": "a9a5efd40eaf558a2bcd48d64b1d1646be686008",
"version": "1.7.1"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"state" : {
"revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
"version" : "1.0.4"
"identity": "swift-async-algorithms",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-async-algorithms.git",
"state": {
"revision": "3da39bbc4e687d4192af7c9cf4eab805745a0b9c",
"version": "1.1.5"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
"identity": "swift-atomics",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-atomics.git",
"state": {
"revision": "0442cb5a3f98ab802acb777929fdb446bda11a34",
"version": "1.3.1"
}
},
{
"identity" : "swift-certificates",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-certificates.git",
"state" : {
"revision" : "f4cd9e78a1ec209b27e426a5f5c693675f95e75a",
"version" : "1.15.0"
"identity": "swift-certificates",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-certificates.git",
"state": {
"revision": "89fbc3714264cce8db8e4ec51b64e01c3e28c6c5",
"version": "1.19.3"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
"version" : "1.2.0"
"identity": "swift-collections",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-collections.git",
"state": {
"revision": "a0cb0954ecb21e4e31b0070e6ed5674e8556685a",
"version": "1.6.0"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "e8d6eba1fef23ae5b359c46b03f7d94be2f41fed",
"version" : "3.12.3"
"identity": "swift-configuration",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-configuration.git",
"state": {
"revision": "be76c4ad929eb6c4bcaf3351799f2adf9e6848a9",
"version": "1.2.0"
}
},
{
"identity" : "swift-distributed-tracing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-distributed-tracing.git",
"state" : {
"revision" : "dc4030184203ffafbb2ec614352487235d747fe0",
"version" : "1.4.1"
"identity": "swift-crypto",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-crypto.git",
"state": {
"revision": "95ba0316a9b733e92bb6b071255ff46263bbe7dc",
"version": "3.15.1"
}
},
{
"identity" : "swift-http-structured-headers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-structured-headers.git",
"state" : {
"revision" : "db6eea3692638a65e2124990155cd220c2915903",
"version" : "1.3.0"
"identity": "swift-distributed-tracing",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-distributed-tracing.git",
"state": {
"revision": "dc4030184203ffafbb2ec614352487235d747fe0",
"version": "1.4.1"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types.git",
"state" : {
"revision" : "a0a57e949a8903563aba4615869310c0ebf14c03",
"version" : "1.4.0"
"identity": "swift-http-structured-headers",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-http-structured-headers.git",
"state": {
"revision": "933538faa42c432d385f02e07df0ace7c5ecfc47",
"version": "1.7.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "bbd81b6725ae874c69e9b8c8804d462356b55523",
"version" : "1.10.1"
"identity": "swift-http-types",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-http-types.git",
"state": {
"revision": "db774a277f60063a32d854f2980299caf06da041",
"version": "1.6.0"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "4e8f4b1c9adaa59315c523540c1ff2b38adc20a9",
"version" : "2.87.0"
"identity": "swift-log",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-log.git",
"state": {
"revision": "a878e7f8f46cfc0e1125e565b5c08e7d5272dc9a",
"version": "1.14.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "145db1962f4f33a4ea07a32e751d5217602eea29",
"version" : "1.28.0"
"identity": "swift-nio",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-nio.git",
"state": {
"revision": "cd3e1152083706d77b223fb29110e590efcc70c0",
"version": "2.101.2"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "5e9e99ec96c53bc2c18ddd10c1e25a3cd97c55e5",
"version" : "1.38.0"
"identity": "swift-nio-extras",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-nio-extras.git",
"state": {
"revision": "88a51340f59cf181ebde888bd1b749296b3ec029",
"version": "1.34.3"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "173cc69a058623525a58ae6710e2f5727c663793",
"version" : "2.36.0"
"identity": "swift-nio-http2",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-nio-http2.git",
"state": {
"revision": "61d1b44f6e4e118792be1cff88ee2bc0267c6f9a",
"version": "1.44.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "cd1e89816d345d2523b11c55654570acd5cd4c56",
"version" : "1.24.0"
"identity": "swift-nio-ssl",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-nio-ssl.git",
"state": {
"revision": "407d82d5b6cc00e1c3fb83a81b1539b70c788c5e",
"version": "2.37.1"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics.git",
"state" : {
"revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8",
"version" : "1.0.3"
"identity": "swift-nio-transport-services",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-nio-transport-services.git",
"state": {
"revision": "67787bb645a5e67d2edcdfbe48a216cc549222d5",
"version": "1.28.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "86970144a0b86068c81ff48ee29b3f97cae0b879",
"version" : "1.36.0"
"identity": "swift-numerics",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-numerics.git",
"state": {
"revision": "0c0290ff6b24942dadb83a929ffaaa1481df04a2",
"version": "1.1.1"
}
},
{
"identity" : "swift-service-context",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-service-context.git",
"state" : {
"revision" : "d0997351b0c7779017f88e7a93bc30a1878d7f29",
"version" : "1.3.0"
"identity": "swift-protobuf",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-protobuf.git",
"state": {
"revision": "55d7a1cc5666b85c13464aea1c4b4a90feccb4c8",
"version": "1.38.1"
}
},
{
"identity" : "swift-service-lifecycle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/swift-service-lifecycle.git",
"state" : {
"revision" : "e7187309187695115033536e8fc9b2eb87fd956d",
"version" : "2.8.0"
"identity": "swift-service-context",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-service-context.git",
"state": {
"revision": "d0997351b0c7779017f88e7a93bc30a1878d7f29",
"version": "1.3.0"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df",
"version" : "1.6.4"
"identity": "swift-service-lifecycle",
"kind": "remoteSourceControl",
"location": "https://github.com/swift-server/swift-service-lifecycle.git",
"state": {
"revision": "9829955b385e5bb88128b73f1b8389e9b9c3191a",
"version": "2.11.0"
}
},
{
"identity" : "zstd",
"kind" : "remoteSourceControl",
"location" : "https://github.com/facebook/zstd.git",
"state" : {
"revision" : "f8745da6ff1ad1e7bab384bd1f9d742439278e99",
"version" : "1.5.7"
"identity": "swift-system",
"kind": "remoteSourceControl",
"location": "https://github.com/apple/swift-system.git",
"state": {
"revision": "b5544ba79a70a0cb3563e75bf26dc198d6b40ed3",
"version": "1.7.4"
}
},
{
"identity": "zstd",
"kind": "remoteSourceControl",
"location": "https://github.com/facebook/zstd.git",
"state": {
"revision": "f8745da6ff1ad1e7bab384bd1f9d742439278e99",
"version": "1.5.7"
}
}
],
"version" : 3
"version": 3
}
+142 -8
View File
@@ -33,6 +33,16 @@ final class IOPair: Sendable {
let buffer: UnsafeMutableBufferPointer<UInt8>
var closed: Bool
var registeredFd: Int32?
// [Nucleic vendored patch] Backpressure state: bytes read from `from` that `to` couldn't
// take yet (its buffer was full), plus whether `to` is currently registered for EPOLLOUT
// to flush them. While `pending` is non-empty the relay reads nothing more — the source
// pipe backs up and throttles the *producing process* instead of this thread. The write
// fd used to be BLOCKING (only registered fds get O_NONBLOCK, and it never was), so one
// slow-drained stream parked the shared ProcessSupervisor poller thread — freezing every
// exec's stdio and every control-plane relay in the container at once.
var pending: [UInt8]
var pendingOffset: Int
var writeFdRegistered: Bool
func drain() {
let readFrom = OSFile(fd: from.fileDescriptor)
@@ -66,10 +76,27 @@ final class IOPair: Sendable {
return
}
// [Nucleic vendored patch] Flush what we can IN ORDER: the pending backlog first,
// then (only if it fully flushed) a best-effort drain of the source. Draining with
// unsent pending bytes would reorder the stream.
let writeTo = OSFile(fd: to.fileDescriptor)
while pendingOffset < pending.count {
let offset = pendingOffset
let result = pending.withUnsafeMutableBufferPointer { buf in
writeTo.write(
UnsafeMutableBufferPointer(
start: buf.baseAddress!.advanced(by: offset),
count: buf.count - offset))
}
if result.wrote > 0 { pendingOffset += result.wrote }
if result.action != .success { break }
}
if pendingOffset >= pending.count {
// Try and drain IO first.
self.drain()
}
// Remove the fd from our global epoll instance first.
// Remove the fds from our global epoll instance first.
if let fd = self.registeredFd {
do {
try ProcessSupervisor.default.unregisterFd(fd)
@@ -78,6 +105,15 @@ final class IOPair: Sendable {
}
self.registeredFd = nil
}
// [Nucleic vendored patch] The write fd may be registered for backpressure flushing.
if self.writeFdRegistered {
do {
try ProcessSupervisor.default.unregisterFd(to.fileDescriptor)
} catch {
logger?.error("failed to delete write fd from epoll \(to.fileDescriptor): \(error)")
}
self.writeFdRegistered = false
}
do {
try self.from.close()
@@ -108,7 +144,10 @@ final class IOPair: Sendable {
to: writeTo,
buffer: buffer,
closed: false,
registeredFd: nil
registeredFd: nil,
pending: [],
pendingOffset: 0,
writeFdRegistered: false
))
self.reason = reason
self.logger = logger
@@ -122,8 +161,16 @@ final class IOPair: Sendable {
return (io.from.fileDescriptor, io.to.fileDescriptor)
}
let readFrom = OSFile(fd: readFromFd)
let writeTo = OSFile(fd: writeToFd)
// [Nucleic vendored patch] The write fd must be non-blocking BEFORE the first relay write.
// `Epoll.add` only sets O_NONBLOCK on fds it registers, and the write fd is registered only
// on demand (EPOLLOUT backpressure below) — so without this, the very first full-buffer
// write blocked the shared poller thread.
let flags = fcntl(writeToFd, F_GETFL)
if flags == -1 || fcntl(writeToFd, F_SETFL, flags | O_NONBLOCK) == -1 {
self.logger?.error(
"failed to set relay write fd non-blocking",
metadata: ["fd": "\(writeToFd)", "errno": "\(errno)"])
}
try ProcessSupervisor.default.registerFd(readFromFd, mask: .input) { mask in
self.io.withLock { io in
@@ -139,7 +186,49 @@ final class IOPair: Sendable {
return
}
// Loop so we drain fully.
self.pump(&io, mask: mask, ignoreHup: ignoreHup)
}
}
}
/// [Nucleic vendored patch] One relay pass, non-blocking end to end: flush any pending
/// backlog toward `to`, then (only once it's empty) drain `from`. On a full destination the
/// remainder is stashed in `pending` and the write fd registered for EPOLLOUT, whose edge
/// re-enters this pump — so backpressure suspends the relay instead of blocking or spinning
/// the shared poller thread. Must be called with the `io` lock held.
private func pump(_ io: inout IO, mask: Epoll.Mask, ignoreHup: Bool) {
let readFrom = OSFile(fd: io.from.fileDescriptor)
let writeTo = OSFile(fd: io.to.fileDescriptor)
// Flush the pending backlog first; reads stay suspended until it clears.
while io.pendingOffset < io.pending.count {
let offset = io.pendingOffset
let result = io.pending.withUnsafeMutableBufferPointer { buf in
writeTo.write(
UnsafeMutableBufferPointer(
start: buf.baseAddress!.advanced(by: offset),
count: buf.count - offset))
}
if result.wrote > 0 { io.pendingOffset += result.wrote }
switch result.action {
case .success:
continue
case .again:
self.ensureWriteRegistered(&io)
return
default:
self.logger?.error("stopping relay: write failed during backlog flush")
io.close(logger: self.logger)
return
}
}
if !io.pending.isEmpty {
io.pending = []
io.pendingOffset = 0
self.unregisterWrite(&io)
}
// Loop so we drain fully (edge-triggered epoll requires reading until EAGAIN).
while true {
let r = readFrom.read(io.buffer)
if r.read > 0 {
@@ -150,24 +239,38 @@ final class IOPair: Sendable {
let w = writeTo.write(view)
if w.wrote != r.read {
switch w.action {
case .again:
// Destination full: stash the remainder and suspend reads until its
// EPOLLOUT edge flushes it. (A later `read` re-reports EOF if this
// chunk was the stream's last, so no EOF is lost by returning here.)
io.pending = Array(
UnsafeBufferPointer(
start: io.buffer.baseAddress!.advanced(by: w.wrote),
count: r.read - w.wrote))
io.pendingOffset = 0
self.ensureWriteRegistered(&io)
return
default:
self.logger?.error("stopping relay: short write for stdio")
io.close(logger: self.logger)
return
}
}
}
switch r.action {
case .error(let errno):
self.logger?.error("failed with errno \(errno) while reading for fd \(readFromFd)")
self.logger?.error("failed with errno \(errno) while reading for fd \(io.from.fileDescriptor)")
fallthrough
case .eof:
self.logger?.debug("closing relay for \(readFromFd)")
self.logger?.debug("closing relay for \(io.from.fileDescriptor)")
io.close(logger: self.logger)
return
case .again:
if mask.isHangup && !ignoreHup {
self.logger?.error("received EPOLLHUP and EAGAIN exiting")
self.close()
io.close(logger: self.logger)
}
return
default:
@@ -175,6 +278,37 @@ final class IOPair: Sendable {
}
}
}
/// [Nucleic vendored patch] Register the write fd for EPOLLOUT so the pending backlog is
/// flushed when the destination drains. Registration failure closes the pair — without the
/// flush wakeup the relay would hang with data stranded. Must be called with the lock held.
private func ensureWriteRegistered(_ io: inout IO) {
guard !io.writeFdRegistered else { return }
let writeToFd = io.to.fileDescriptor
do {
try ProcessSupervisor.default.registerFd(writeToFd, mask: .output) { _ in
self.io.withLock { io in
guard !io.closed else { return }
// An empty mask: HUP/EOF handling rides the read fd's own events.
self.pump(&io, mask: [], ignoreHup: true)
}
}
io.writeFdRegistered = true
} catch {
self.logger?.error("failed to register relay write fd for backpressure: \(error)")
io.close(logger: self.logger)
}
}
/// [Nucleic vendored patch] Drop the EPOLLOUT registration once the backlog has flushed.
/// Must be called with the lock held.
private func unregisterWrite(_ io: inout IO) {
guard io.writeFdRegistered else { return }
io.writeFdRegistered = false
do {
try ProcessSupervisor.default.unregisterFd(io.to.fileDescriptor)
} catch {
self.logger?.error("failed to unregister relay write fd: \(error)")
}
}
@@ -89,7 +89,17 @@ extension OSFile {
if errno != EAGAIN && errno != EIO {
throw POSIXError(.init(rawValue: errno)!)
}
break
// [Nucleic vendored patch] Destination full: RETURN, don't `break`. Breaking
// sent the outer `while true` straight back here — with the source idle and the
// destination still full, neither leg could progress and this spun the caller's
// thread at 100% until the peer drained. The caller is an epoll handler on
// vminitd's SINGLE ProcessSupervisor poller thread, so the spin froze every
// exec's stdio and every control-plane relay in the container at once (the
// all-sessions "produced no output within 60s" stall / dead control plane).
// The un-flushed bytes stay in the transfer pipe (`from.offset > to.offset`
// persists in the SpliceFiles); the destination fd is registered for EPOLLOUT,
// whose edge re-enters transferData and resumes the flush.
return (from.offset - fromOffset, to.offset - toOffset, .success)
}
to.offset += bytesWrote
if bytesWrote == 0 {
@@ -244,6 +244,22 @@ extension VsockProxy {
try relayTo.connect()
// [Nucleic vendored patch] BOTH fds must be non-blocking BEFORE either is
// registered. `Epoll.add` sets O_NONBLOCK only at registration time, and the first
// (client) registration's handler can fire — and splice toward the server fd —
// before the second (server) registration has made that fd non-blocking. A full
// destination then turned the splice into a genuinely BLOCKING call on vminitd's
// single ProcessSupervisor poller thread, freezing every exec's stdio and every
// control-plane relay in the container until the peer drained.
for fd in [conn.fileDescriptor, relayTo.fileDescriptor] {
let flags = fcntl(fd, F_GETFL)
if flags == -1 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1 {
self.log?.error(
"failed to set proxy fd non-blocking",
metadata: ["fd": "\(fd)", "errno": "\(errno)"])
}
}
// `clientFile` isn't used concurrently.
nonisolated(unsafe) var clientFile = OSFile.SpliceFile(fd: conn.fileDescriptor)
nonisolated(unsafe) var eofFromClient = false