@@ -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 "
0 commit comments