Skip to content

Commit fee41d2

Browse files
authored
Fix file descriptor leak (#315)
1 parent 3d1df53 commit fee41d2

4 files changed

Lines changed: 113 additions & 21 deletions

File tree

Sources/WASI/Platform/Directory.swift

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ extension DirEntry: WASIDir, FdWASIEntry {
9999
}
100100

101101
func removeFile(atPath path: String) throws {
102-
let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path)
102+
let result = try SandboxPrimitives.openParent(start: fd, path: path)
103+
let dir = result.parentFd
104+
let basename = result.basename
103105
try WASIAbi.Errno.translatingPlatformErrno {
104106
try dir.remove(at: FilePath(basename), options: [])
105107
}
@@ -109,17 +111,21 @@ extension DirEntry: WASIDir, FdWASIEntry {
109111
#if os(Windows)
110112
throw WASIAbi.Errno.ENOSYS
111113
#else
112-
let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path)
114+
let result = try SandboxPrimitives.openParent(start: fd, path: path)
115+
let dir = result.parentFd
116+
let basename = result.basename
113117
try WASIAbi.Errno.translatingPlatformErrno {
114118
try dir.remove(at: FilePath(basename), options: .removeDirectory)
115119
}
116120
#endif
117121
}
118122

119123
func symlink(from sourcePath: String, to destPath: String) throws {
120-
let (destDir, destBasename) = try SandboxPrimitives.openParent(
124+
let result = try SandboxPrimitives.openParent(
121125
start: fd, path: destPath
122126
)
127+
let destDir = result.parentFd
128+
let destBasename = result.basename
123129
try WASIAbi.Errno.translatingPlatformErrno {
124130
try destDir.createSymlink(original: FilePath(sourcePath), link: FilePath(destBasename))
125131
}
@@ -142,12 +148,16 @@ extension DirEntry: WASIDir, FdWASIEntry {
142148
let oldPath = SandboxPrimitives.stripDirSuffix(sourcePath)
143149
let newPath = SandboxPrimitives.stripDirSuffix(destPath)
144150

145-
let (sourceDir, sourceBasename) = try SandboxPrimitives.openParent(
151+
let sourceResult = try SandboxPrimitives.openParent(
146152
start: fd, path: oldPath
147153
)
148-
let (destDir, destBasename) = try SandboxPrimitives.openParent(
154+
let sourceDir = sourceResult.parentFd
155+
let sourceBasename = sourceResult.basename
156+
let destResult = try SandboxPrimitives.openParent(
149157
start: newDir.fd, path: newPath
150158
)
159+
let destDir = destResult.parentFd
160+
let destBasename = destResult.basename
151161

152162
// Re-append a slash if the original path had one
153163
let finalSourceBasename = oldHasTrailingSlash ? sourceBasename + "/" : sourceBasename
@@ -204,7 +214,9 @@ extension DirEntry: WASIDir, FdWASIEntry {
204214
}
205215

206216
func createDirectory(atPath path: String) throws {
207-
let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path)
217+
let result = try SandboxPrimitives.openParent(start: fd, path: path)
218+
let dir = result.parentFd
219+
let basename = result.basename
208220
try WASIAbi.Errno.translatingPlatformErrno {
209221
try dir.createDirectory(at: FilePath(basename), permissions: .ownerReadWriteExecute)
210222
}
@@ -218,7 +230,9 @@ extension DirEntry: WASIDir, FdWASIEntry {
218230
if !symlinkFollow {
219231
options.insert(.noFollow)
220232
}
221-
let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path)
233+
let result = try SandboxPrimitives.openParent(start: fd, path: path)
234+
let dir = result.parentFd
235+
let basename = result.basename
222236
let attributes = try basename.withCString { cBasename in
223237
try WASIAbi.Errno.translatingPlatformErrno {
224238
try dir.attributes(at: cBasename, options: options)

Sources/WASI/Platform/SandboxPrimitives/OpenParent.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,50 @@ extension SandboxPrimitives {
4747
return path.hasSuffix("/")
4848
}
4949

50-
static func openParent(start: FileDescriptor, path: String) throws -> (FileDescriptor, String) {
50+
static func openParent(start: FileDescriptor, path: String) throws -> SplitPath {
5151
guard let (dirName, basename) = splitParent(path: path) else {
5252
throw WASIAbi.Errno.ENOENT
5353
}
5454

55-
let dirFd: FileDescriptor
55+
let splitPath: SplitPath
5656
if !dirName.isEmpty {
5757
let options: FileDescriptor.OpenOptions
5858
#if os(Windows)
5959
options = []
6060
#else
6161
options = .directory
6262
#endif
63-
dirFd = try openAt(
64-
start: start, path: dirName,
65-
mode: .readOnly, options: options,
66-
permissions: []
63+
splitPath = try .init(
64+
parentFd: openAt(
65+
start: start, path: dirName,
66+
mode: .readOnly, options: options,
67+
permissions: []
68+
),
69+
basename: basename.string,
70+
isTemporary: true
6771
)
6872
} else {
69-
dirFd = start
73+
splitPath = .init(
74+
parentFd: start,
75+
basename: basename.string,
76+
isTemporary: false
77+
)
78+
}
79+
return splitPath
80+
}
81+
}
82+
83+
/// The return value of `SandboxPrimitives.openParent`.
84+
///
85+
/// If `isTemporary` is `true`, the file descriptor will be closed when the instance is destroyed.
86+
struct SplitPath: ~Copyable {
87+
var parentFd: FileDescriptor
88+
var basename: String
89+
var isTemporary: Bool
90+
91+
deinit {
92+
if isTemporary {
93+
try? parentFd.close()
7094
}
71-
return (dirFd, basename.string)
7295
}
7396
}

Sources/WASI/Platform/SandboxPrimitives/Readlink.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,9 @@ extension SandboxPrimitives {
2929
let initialBufferCapacity = 256
3030
let maxBufferCapacity = max(initialBufferCapacity, Int(PATH_MAX))
3131

32-
let (dir, basename) = try openParent(start: start, path: path)
33-
defer {
34-
if dir.rawValue != start.rawValue {
35-
try? dir.close()
36-
}
37-
}
32+
let result = try openParent(start: start, path: path)
33+
let dir = result.parentFd
34+
let basename = result.basename
3835

3936
return try basename.withCString { cBasename in
4037
var capacity = min(initialBufferCapacity, maxBufferCapacity)

Tests/WASITests/WASITests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,4 +1347,62 @@ struct WASITests {
13471347
}
13481348
#endif
13491349

1350+
#if os(macOS) || os(Linux)
1351+
// https://github.com/swiftwasm/WasmKit/issues/275
1352+
@Test
1353+
func fileDescriptorLeakTest() throws {
1354+
let t = try TestSupport.TemporaryDirectory()
1355+
1356+
try t.createDir(at: "dir1")
1357+
try t.createDir(at: "dir1/foo")
1358+
try t.createDir(at: "dir2")
1359+
try t.createDir(at: "dir2/foo")
1360+
1361+
let wasi = try WASIBridgeToHost(
1362+
fileSystem: .host().withPreopens([
1363+
.init(guestPath: "/dir1", hostPath: t.url.appending(component: "dir1").path),
1364+
.init(guestPath: "/dir2", hostPath: t.url.appending(component: "dir2").path),
1365+
])
1366+
).underlying
1367+
let dir1Fd: WASIAbi.Fd = 3
1368+
let dir2Fd: WASIAbi.Fd = 4
1369+
1370+
let memory = TestSupport.TestGuestMemory()
1371+
let buffer = UnsafeGuestBufferPointer<UInt8>(
1372+
baseAddress: UnsafeGuestPointer<UInt8>(memorySpace: memory, offset: 0),
1373+
count: 4096
1374+
)
1375+
let writeData = Array("hello".utf8)
1376+
let writeVecs = memory.writeIOVecs([writeData])
1377+
1378+
for _ in 0..<1000 {
1379+
let fd = try wasi.path_open(
1380+
dirFd: dir1Fd,
1381+
dirFlags: [],
1382+
path: "foo/bar",
1383+
oflags: [.CREAT],
1384+
fsRightsBase: [.FD_WRITE],
1385+
fsRightsInheriting: [],
1386+
fdflags: []
1387+
)
1388+
#expect(try wasi.fd_write(fileDescriptor: fd, ioVectors: writeVecs) > 0)
1389+
try wasi.fd_close(fd: fd)
1390+
1391+
try wasi.path_rename(oldFd: dir1Fd, oldPath: "foo/bar", newFd: dir2Fd, newPath: "foo/baz")
1392+
1393+
try wasi.path_symlink(oldPath: "baz", dirFd: dir2Fd, newPath: "foo/quux")
1394+
let stat = try wasi.path_filestat_get(dirFd: dir2Fd, flags: .SYMLINK_FOLLOW, path: "foo/quux")
1395+
#expect(stat.size == 5) // hello
1396+
let count = try Int(wasi.path_readlink(fd: dir2Fd, path: "foo/quux", buffer: buffer))
1397+
buffer.withHostPointer { ptr in
1398+
#expect(String(decoding: ptr[..<count], as: UTF8.self) == "baz")
1399+
}
1400+
try wasi.path_unlink_file(dirFd: dir2Fd, path: "foo/baz")
1401+
try wasi.path_unlink_file(dirFd: dir2Fd, path: "foo/quux")
1402+
1403+
try wasi.path_create_directory(dirFd: dir1Fd, path: "foo/bar")
1404+
try wasi.path_remove_directory(dirFd: dir1Fd, path: "foo/bar")
1405+
}
1406+
}
1407+
#endif
13501408
}

0 commit comments

Comments
 (0)