Skip to content

Commit b778595

Browse files
authored
Various bug fixes throughout the code base: (#171)
1 parent 6eb91f6 commit b778595

File tree

4 files changed

+113
-57
lines changed

4 files changed

+113
-57
lines changed

Sources/Subprocess/AsyncBufferSequence.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ extension AsyncBufferSequence {
143143
private var source: AsyncBufferSequence.AsyncIterator
144144
private var buffer: [Encoding.CodeUnit]
145145
private var underlyingBuffer: [Encoding.CodeUnit]
146+
private var underlyingBufferIndex: Array<Encoding.CodeUnit>.Index
146147
private var leftover: Encoding.CodeUnit?
147148
private var eofReached: Bool
148149
private let bufferingPolicy: BufferingPolicy
@@ -154,6 +155,7 @@ extension AsyncBufferSequence {
154155
self.source = underlyingIterator
155156
self.buffer = []
156157
self.underlyingBuffer = []
158+
self.underlyingBufferIndex = self.underlyingBuffer.startIndex
157159
self.leftover = nil
158160
self.eofReached = false
159161
self.bufferingPolicy = bufferingPolicy
@@ -208,13 +210,16 @@ extension AsyncBufferSequence {
208210
}
209211

210212
func nextFromSource() async throws -> Encoding.CodeUnit? {
211-
if underlyingBuffer.isEmpty {
213+
if underlyingBufferIndex >= underlyingBuffer.count {
212214
guard let buf = try await loadBuffer() else {
213215
return nil
214216
}
215217
underlyingBuffer = buf
218+
underlyingBufferIndex = buf.startIndex
216219
}
217-
return underlyingBuffer.removeFirst()
220+
let result = underlyingBuffer[underlyingBufferIndex]
221+
underlyingBufferIndex = underlyingBufferIndex.advanced(by: 1)
222+
return result
218223
}
219224

220225
func nextCodeUnit() async throws -> Encoding.CodeUnit? {

Sources/Subprocess/Configuration.swift

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,33 +187,73 @@ extension Configuration {
187187
) throws {
188188
var possibleError: (any Swift.Error)? = nil
189189

190+
// To avoid closing the same descriptor multiple times,
191+
// keep track of the list of descriptors that we have
192+
// already closed. If a `IODescriptor.Descriptor` is
193+
// already closed, mark that `IODescriptor` as closed
194+
// as opposed to actually try to close it.
195+
var remainingSet: Set<IODescriptor.Descriptor> = Set(
196+
optionalSequence: [
197+
inputRead?.descriptor,
198+
inputWrite?.descriptor,
199+
outputRead?.descriptor,
200+
outputWrite?.descriptor,
201+
errorRead?.descriptor,
202+
errorWrite?.descriptor,
203+
]
204+
)
205+
190206
do {
191-
try inputRead?.safelyClose()
207+
if remainingSet.tryRemove(inputRead?.descriptor) {
208+
try inputRead?.safelyClose()
209+
} else {
210+
try inputRead?.markAsClosed()
211+
}
192212
} catch {
193213
possibleError = error
194214
}
195215
do {
196-
try inputWrite?.safelyClose()
216+
if remainingSet.tryRemove(inputWrite?.descriptor) {
217+
try inputWrite?.safelyClose()
218+
} else {
219+
try inputWrite?.markAsClosed()
220+
}
197221
} catch {
198222
possibleError = error
199223
}
200224
do {
201-
try outputRead?.safelyClose()
225+
if remainingSet.tryRemove(outputRead?.descriptor) {
226+
try outputRead?.safelyClose()
227+
} else {
228+
try outputRead?.markAsClosed()
229+
}
202230
} catch {
203231
possibleError = error
204232
}
205233
do {
206-
try outputWrite?.safelyClose()
234+
if remainingSet.tryRemove(outputWrite?.descriptor) {
235+
try outputWrite?.safelyClose()
236+
} else {
237+
try outputWrite?.markAsClosed()
238+
}
207239
} catch {
208240
possibleError = error
209241
}
210242
do {
211-
try errorRead?.safelyClose()
243+
if remainingSet.tryRemove(errorRead?.descriptor) {
244+
try errorRead?.safelyClose()
245+
} else {
246+
try errorRead?.markAsClosed()
247+
}
212248
} catch {
213249
possibleError = error
214250
}
215251
do {
216-
try errorWrite?.safelyClose()
252+
if remainingSet.tryRemove(errorWrite?.descriptor) {
253+
try errorWrite?.safelyClose()
254+
} else {
255+
try errorWrite?.markAsClosed()
256+
}
217257
} catch {
218258
possibleError = error
219259
}
@@ -733,6 +773,10 @@ internal struct IODescriptor: ~Copyable {
733773
#endif
734774
}
735775

776+
internal mutating func markAsClosed() throws {
777+
self.closeWhenDone = false
778+
}
779+
736780
deinit {
737781
guard self.closeWhenDone else {
738782
return
@@ -1081,3 +1125,17 @@ extension _OrderedSet: Sequence {
10811125
return self.elements.makeIterator()
10821126
}
10831127
}
1128+
1129+
extension Set {
1130+
init<S>(optionalSequence sequence: S) where S: Sequence, S.Element == Optional<Self.Element> {
1131+
let sequence: [Self.Element] = sequence.compactMap(\.self)
1132+
self.init(sequence)
1133+
}
1134+
1135+
mutating func tryRemove(_ element: Self.Element?) -> Bool {
1136+
guard let element else {
1137+
return false
1138+
}
1139+
return self.remove(element) != nil
1140+
}
1141+
}

Sources/_SubprocessCShims/process_shims.c

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,13 @@ static int _positive_int_parse(const char *str) {
431431
}
432432

433433
#if defined(__linux__)
434-
// Linux-specific version that uses syscalls directly and doesn't allocate heap memory.
435-
// Safe to use after vfork() and before execve()
436-
static int _highest_possibly_open_fd_dir_linux(const char *fd_dir) {
437-
int highest_fd_so_far = 0;
434+
/// Set `FD_CLOEXEC` on all open file descriptors listed under `fd_dir` so
435+
/// they are automatically closed upon `execve()`.
436+
/// Safe to use after `vfork()` and before `execve()`
437+
static void _set_cloexec_to_open_fds(const char *fd_dir) {
438438
int dir_fd = open(fd_dir, O_RDONLY);
439439
if (dir_fd < 0) {
440-
// errno set by `open`.
441-
return -1;
440+
return;
442441
}
443442

444443
// Buffer for directory entries - allocated on stack, no heap allocation
@@ -450,49 +449,37 @@ static int _highest_possibly_open_fd_dir_linux(const char *fd_dir) {
450449
if (errno == EINTR) {
451450
continue;
452451
} else {
453-
// `errno` set by _getdents64.
454-
highest_fd_so_far = -1;
455452
close(dir_fd);
456-
return highest_fd_so_far;
453+
return;
457454
}
458455
}
459456
if (bytes_read == 0) {
460457
close(dir_fd);
461-
return highest_fd_so_far;
458+
return;
462459
}
463460
long offset = 0;
464461
while (offset < bytes_read) {
465462
struct linux_dirent64 *entry = (struct linux_dirent64 *)(buffer + offset);
466-
467463
// Skip "." and ".." entries
468464
if (entry->d_name[0] != '.') {
469-
int number = _positive_int_parse(entry->d_name);
470-
if (number > highest_fd_so_far) {
471-
highest_fd_so_far = number;
465+
int fd = _positive_int_parse(entry->d_name);
466+
if (fd > STDERR_FILENO && fd != dir_fd) {
467+
int flags = fcntl(fd, F_GETFD);
468+
if (flags >= 0) {
469+
// Set FD_CLOEXEC on every open fd so they are closed after exec()
470+
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
471+
}
472472
}
473473
}
474-
475474
offset += entry->d_reclen;
476475
}
477476
}
478-
479-
close(dir_fd);
480-
return highest_fd_so_far;
481477
}
482478
#endif
483479

484-
// This function is only used on systems with Linux kernel 5.9 or lower.
485-
// On newer systems, `close_range` is used instead.
480+
// This function is only used on non-Linux systems.
486481
static int _highest_possibly_open_fd(void) {
487-
#if defined(__linux__)
488-
int hi = _highest_possibly_open_fd_dir_linux("/dev/fd");
489-
if (hi < 0) {
490-
hi = sysconf(_SC_OPEN_MAX);
491-
}
492-
#else
493-
int hi = sysconf(_SC_OPEN_MAX);
494-
#endif
495-
return hi;
482+
return sysconf(_SC_OPEN_MAX);
496483
}
497484

498485
int _subprocess_fork_exec(
@@ -681,8 +668,8 @@ int _subprocess_fork_exec(
681668
errno = ENOSYS;
682669
#if (__has_include(<linux/close_range.h>) && (!defined(__ANDROID__) || __ANDROID_API__ >= 34)) || defined(__FreeBSD__)
683670
// We must NOT close pipefd[1] for writing errors
684-
rc = close_range(STDERR_FILENO + 1, pipefd[1] - 1, 0);
685-
rc |= close_range(pipefd[1] + 1, ~0U, 0);
671+
rc = close_range(STDERR_FILENO + 1, pipefd[1] - 1, CLOSE_RANGE_CLOEXEC);
672+
rc |= close_range(pipefd[1] + 1, ~0U, CLOSE_RANGE_CLOEXEC);
686673
#elif defined(__OpenBSD__)
687674
// OpenBSD Supports closefrom, but not close_range
688675
// See https://man.openbsd.org/closefrom
@@ -692,13 +679,22 @@ int _subprocess_fork_exec(
692679
rc = closefrom(pipefd[1] + 1);
693680
#endif
694681
if (rc != 0) {
695-
// close_range failed (or doesn't exist), fall back to close()
696-
for (int fd = STDERR_FILENO + 1; fd <= _highest_possibly_open_fd(); fd++) {
682+
#if defined(__linux__)
683+
_set_cloexec_to_open_fds("/dev/fd");
684+
#else
685+
// close_range failed (or doesn't exist), fall back to setting FD_CLOEXEC
686+
int highest_open_fd = _highest_possibly_open_fd();
687+
for (int fd = STDERR_FILENO + 1; fd <= highest_open_fd; fd++) {
697688
// We must NOT close pipefd[1] for writing errors
698689
if (fd != pipefd[1]) {
699-
close(fd);
690+
int flags = fcntl(fd, F_GETFD);
691+
if (flags >= 0) {
692+
// Set FD_CLOEXEC on every open fd so they are closed after exec()
693+
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
694+
}
700695
}
701696
}
697+
#endif
702698
}
703699

704700
// Finally, exec

Tests/SubprocessTests/IntegrationTests.swift

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,22 +1337,19 @@ extension SubprocessIntegrationTests {
13371337
options: .create,
13381338
permissions: [.ownerReadWrite, .groupReadWrite]
13391339
)
1340-
let echoResult = try await outputFile.closeAfter {
1341-
let echoResult = try await _run(
1342-
setup,
1343-
input: .none,
1344-
output: .fileDescriptor(
1345-
outputFile,
1346-
closeAfterSpawningProcess: false
1347-
),
1348-
error: .fileDescriptor(
1349-
outputFile,
1350-
closeAfterSpawningProcess: false
1351-
)
1340+
let echoResult = try await _run(
1341+
setup,
1342+
input: .none,
1343+
output: .fileDescriptor(
1344+
outputFile,
1345+
closeAfterSpawningProcess: true
1346+
),
1347+
error: .fileDescriptor(
1348+
outputFile,
1349+
closeAfterSpawningProcess: true
13521350
)
1353-
#expect(echoResult.terminationStatus.isSuccess)
1354-
return echoResult
1355-
}
1351+
)
1352+
#expect(echoResult.terminationStatus.isSuccess)
13561353
let outputData: Data = try Data(
13571354
contentsOf: URL(filePath: outputFilePath.string)
13581355
)

0 commit comments

Comments
 (0)