Skip to content

Commit 8140499

Browse files
authored
TerminalIO: Fix ebadf on close (#719)
The stdout and stdin IOPairs in TerminalIO share the same Terminal fd. Closing stdin first would close the Terminal, causing stdout to read -1 from fileDescriptor and fail to unregister from epoll. This left stale handler closures (capturing the IOPair) in ProcessSupervisor.handlers indefinitely. Additionally, vsock sockets created in TerminalIO.start() with closeOnDeinit disabled were never closed if the process exited before attach() created IOPairs.
1 parent 9d2b579 commit 8140499

3 files changed

Lines changed: 46 additions & 11 deletions

File tree

vminitd/Sources/VminitdCore/IOCloser.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,15 @@ protocol IOCloser: Sendable {
1919

2020
func close() throws
2121
}
22+
23+
struct UnownedIOCloser: IOCloser {
24+
private let inner: IOCloser
25+
26+
var fileDescriptor: Int32 { inner.fileDescriptor }
27+
28+
init(_ inner: IOCloser) {
29+
self.inner = inner
30+
}
31+
32+
func close() throws {}
33+
}

vminitd/Sources/VminitdCore/IOPair.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ final class IOPair: Sendable {
3030
let to: IOCloser
3131
let buffer: UnsafeMutableBufferPointer<UInt8>
3232
var closed: Bool
33+
var registeredFd: Int32?
3334

3435
func drain() {
3536
let readFrom = OSFile(fd: from.fileDescriptor)
@@ -67,11 +68,13 @@ final class IOPair: Sendable {
6768
self.drain()
6869

6970
// Remove the fd from our global epoll instance first.
70-
let readFromFd = self.from.fileDescriptor
71-
do {
72-
try ProcessSupervisor.default.unregisterFd(readFromFd)
73-
} catch {
74-
logger?.error("failed to delete fd from epoll \(readFromFd): \(error)")
71+
if let fd = self.registeredFd {
72+
do {
73+
try ProcessSupervisor.default.unregisterFd(fd)
74+
} catch {
75+
logger?.error("failed to delete fd from epoll \(fd): \(error)")
76+
}
77+
self.registeredFd = nil
7578
}
7679

7780
do {
@@ -102,7 +105,8 @@ final class IOPair: Sendable {
102105
from: readFrom,
103106
to: writeTo,
104107
buffer: buffer,
105-
closed: false
108+
closed: false,
109+
registeredFd: nil
106110
))
107111
self.reason = reason
108112
self.logger = logger
@@ -112,7 +116,8 @@ final class IOPair: Sendable {
112116
self.logger?.info("setting up relay for \(reason)")
113117

114118
let (readFromFd, writeToFd) = self.io.withLock { io in
115-
(io.from.fileDescriptor, io.to.fileDescriptor)
119+
io.registeredFd = io.from.fileDescriptor
120+
return (io.from.fileDescriptor, io.to.fileDescriptor)
116121
}
117122

118123
let readFrom = OSFile(fd: readFromFd)

vminitd/Sources/VminitdCore/TerminalIO.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ final class TerminalIO: ManagedProcess.IO & Sendable {
9898
if let stdinSocket = $0.stdinSocket {
9999
let pair = IOPair(
100100
readFrom: stdinSocket,
101-
writeTo: term,
101+
writeTo: UnownedIOCloser(term),
102102
reason: "TerminalIO stdin",
103103
logger: log
104104
)
@@ -121,14 +121,32 @@ final class TerminalIO: ManagedProcess.IO & Sendable {
121121

122122
func close() throws {
123123
self.state.withLock {
124+
// stdout must close before stdin because both IOPairs share the
125+
// Terminal fd. stdout registered that fd with epoll (as its read
126+
// source) and needs to unregister it while the fd is still valid.
127+
// stdin closes the Terminal as its write destination, which would
128+
// invalidate the fd before stdout can unregister.
129+
if let stdout = $0.stdout {
130+
stdout.close()
131+
$0.stdout = nil
132+
}
124133
if let stdin = $0.stdin {
125134
stdin.close()
126135
$0.stdin = nil
127136
}
128-
if let stdout = $0.stdout {
129-
stdout.close()
130-
$0.stdout = nil
137+
138+
// If IOPairs were never created (process exited before attach),
139+
// close the raw sockets directly since they have closeOnDeinit
140+
// disabled.
141+
if let stdinSocket = $0.stdinSocket {
142+
try? stdinSocket.close()
143+
$0.stdinSocket = nil
131144
}
145+
if let stdoutSocket = $0.stdoutSocket {
146+
try? stdoutSocket.close()
147+
$0.stdoutSocket = nil
148+
}
149+
132150
$0.parent = nil
133151
}
134152
}

0 commit comments

Comments
 (0)