Skip to content

Commit fe7f854

Browse files
committed
Add integration test for deferred vsock connection stability
Exercises the dialAgent() → gRPC RPC path with deliberate delays between creating the connection and making the first RPC call. This reproduces a crash where NIO hits a precondition failure (EBADF) in fcntl(F_SETNOSIGPIPE) because the VZVirtioSocketConnection was closed before the gRPC client created the NIO channel.
1 parent 28f1c5a commit fe7f854

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

Sources/Integration/ContainerTests.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3969,6 +3969,70 @@ extension IntegrationSuite {
39693969
}
39703970
}
39713971

3972+
/// Exercises the dialAgent() → gRPC RPC path that previously crashed with
3973+
/// EBADF when the VZVirtioSocketConnection was closed before the gRPC
3974+
/// client made its first call.
3975+
///
3976+
/// Each exec() call creates a new vsock connection via dialAgent(). The
3977+
/// gRPC ClientConnection defers NIO channel creation until the first RPC
3978+
/// (createProcess). A delay between exec() and start() widens the window
3979+
/// where the fd must remain valid — if the VZVirtioSocketConnection is
3980+
/// closed prematurely, the fd may be invalidated by the time NIO tries
3981+
/// fcntl(F_SETNOSIGPIPE), causing a precondition failure.
3982+
func testExecDeferredConnectionStability() async throws {
3983+
let id = "test-exec-deferred-connection-stability"
3984+
3985+
let bs = try await bootstrap(id)
3986+
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
3987+
config.process.arguments = ["/bin/sleep", "1000"]
3988+
config.bootLog = bs.bootLog
3989+
}
3990+
3991+
do {
3992+
try await container.create()
3993+
try await container.start()
3994+
3995+
// Run multiple sequential exec calls with delays between creating the
3996+
// gRPC connection (exec) and making the first RPC (start). This is the
3997+
// pattern that triggered the EBADF crash: the fd was dup'd, the
3998+
// VZVirtioSocketConnection was closed, and by the time NIO tried to
3999+
// create the channel the fd was invalid.
4000+
for i in 0..<10 {
4001+
let buffer = BufferWriter()
4002+
let exec = try await container.exec("deferred-\(i)") { config in
4003+
config.arguments = ["/bin/echo", "exec-\(i)"]
4004+
config.stdout = buffer
4005+
}
4006+
4007+
// Delay between exec() (which calls dialAgent/creates gRPC connection)
4008+
// and start() (which triggers the first RPC/NIO channel creation).
4009+
try await Task.sleep(for: .milliseconds(100))
4010+
4011+
try await exec.start()
4012+
let status = try await exec.wait()
4013+
try await exec.delete()
4014+
4015+
guard status.exitCode == 0 else {
4016+
throw IntegrationError.assert(msg: "exec deferred-\(i) status \(status) != 0")
4017+
}
4018+
4019+
guard let output = String(data: buffer.data, encoding: .utf8) else {
4020+
throw IntegrationError.assert(msg: "failed to read output from deferred-\(i)")
4021+
}
4022+
guard output.trimmingCharacters(in: .whitespacesAndNewlines) == "exec-\(i)" else {
4023+
throw IntegrationError.assert(msg: "deferred-\(i) output mismatch: \(output)")
4024+
}
4025+
}
4026+
4027+
try await container.kill(SIGKILL)
4028+
try await container.wait()
4029+
try await container.stop()
4030+
} catch {
4031+
try? await container.stop()
4032+
throw error
4033+
}
4034+
}
4035+
39724036
@available(macOS 26.0, *)
39734037
func testNetworkingDisabled() async throws {
39744038
let id = "test-networking-disabled"

Sources/Integration/Suite.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ struct IntegrationSuite: AsyncParsableCommand {
369369
Test("container useInit zombie reaping", testUseInitZombieReaping),
370370
Test("container useInit with terminal", testUseInitWithTerminal),
371371
Test("container useInit with stdin", testUseInitWithStdin),
372+
Test("exec deferred connection stability", testExecDeferredConnectionStability),
372373

373374
// Pods
374375
Test("pod single container", testPodSingleContainer),

0 commit comments

Comments
 (0)