Skip to content

WASI: Implement path_open with proper symlink resolution #161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions Sources/WASI/Platform/PlatformTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,23 @@ extension WASIAbi.Errno {
do {
return try body()
} catch let errno as Errno {
guard let error = WASIAbi.Errno(platformErrno: errno) else {
throw WASIError(description: "Unknown underlying OS error: \(errno)")
}
throw error
throw try WASIAbi.Errno(platformErrno: errno)
}
}

init(platformErrno: CInt) throws {
try self.init(platformErrno: SystemPackage.Errno(rawValue: platformErrno))
}

init(platformErrno: Errno) throws {
guard let error = WASIAbi.Errno(_platformErrno: platformErrno) else {
throw WASIError(description: "Unknown underlying OS error: \(platformErrno)")
}
self = error
}

init?(platformErrno: SystemPackage.Errno) {
switch platformErrno {
private init?(_platformErrno: SystemPackage.Errno) {
switch _platformErrno {
case .permissionDenied: self = .EPERM
case .notPermitted: self = .EPERM
case .noSuchFileOrDirectory: self = .ENOENT
Expand Down
134 changes: 120 additions & 14 deletions Sources/WASI/Platform/SandboxPrimitives/Open.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import SystemExtras
import SystemPackage

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import CSystem
import Glibc
#elseif canImport(Musl)
import CSystem
import Musl
#elseif os(Windows)
import CSystem
import ucrt
#else
#error("Unsupported Platform")
#endif

struct PathResolution {
private let mode: FileDescriptor.AccessMode
private let options: FileDescriptor.OpenOptions
Expand All @@ -10,7 +25,20 @@ struct PathResolution {
private let path: FilePath
private var openDirectories: [FileDescriptor]
/// Reverse-ordered remaining path components
/// File name appears first, then parent directories.
/// e.g. `a/b/c` -> ["c", "b", "a"]
/// This ordering is just to avoid dropFirst() on Array.
private var components: FilePath.ComponentView
private var resolvedSymlinks: Int = 0

private static var MAX_SYMLINKS: Int {
// Linux defines MAXSYMLINKS as 40, but on darwin platforms, it's 32.
// Take a single conservative value here to avoid platform-specific
// behavior as much as possible.
// * https://github.com/apple-oss-distributions/xnu/blob/8d741a5de7ff4191bf97d57b9f54c2f6d4a15585/bsd/sys/param.h#L207
// * https://github.com/torvalds/linux/blob/850925a8133c73c4a2453c360b2c3beb3bab67c9/include/linux/namei.h#L13
return 32
}

init(
baseDirFd: FileDescriptor,
Expand All @@ -33,39 +61,117 @@ struct PathResolution {
// no more parent directory means too many `..`
throw WASIAbi.Errno.EPERM
}
try self.baseFd.close()
self.baseFd = lastDirectory
}

mutating func regular(component: FilePath.Component) throws {
let options: FileDescriptor.OpenOptions
var options: FileDescriptor.OpenOptions = []
#if !os(Windows)
// First, try without following symlinks as a fast path.
// If it's actually a symlink and options don't have O_NOFOLLOW,
// we'll try again with interpreting resolved symlink.
options.insert(.noFollow)
#endif
let mode: FileDescriptor.AccessMode
if !self.components.isEmpty {
var intermediateOptions: FileDescriptor.OpenOptions = []

if !self.components.isEmpty {
#if !os(Windows)
// When trying to open an intermediate directory,
// we can assume it's directory.
intermediateOptions.insert(.directory)
// FIXME: Resolve symlink in safe way
intermediateOptions.insert(.noFollow)
options.insert(.directory)
#endif
options = intermediateOptions
mode = .readOnly
} else {
options = self.options
options.formUnion(self.options)
mode = self.mode
}

try WASIAbi.Errno.translatingPlatformErrno {
let newFd = try self.baseFd.open(
at: FilePath(root: nil, components: component),
mode, options: options, permissions: permissions
)
self.openDirectories.append(self.baseFd)
self.baseFd = newFd
do {
let newFd = try self.baseFd.open(
at: FilePath(root: nil, components: component),
mode, options: options, permissions: permissions
)
self.openDirectories.append(self.baseFd)
self.baseFd = newFd
return
} catch let openErrno as Errno {
#if os(Windows)
// Windows doesn't have O_NOFOLLOW, so we can't retry with following symlink.
throw openErrno
#else
if self.options.contains(.noFollow) {
// If "open" failed with O_NOFOLLOW, no need to retry.
throw openErrno
}

// If "open" failed and it might be a symlink, try again with following symlink.

// Check if it's a symlink by fstatat(2).
//
// NOTE: `errno` has enough information to check if the component is a symlink,
// but the value is platform-specific (e.g. ELOOP on POSIX standards, but EMLINK
// on BSD family), so we conservatively check it by fstatat(2).
let attrs = try self.baseFd.attributes(
at: FilePath(root: nil, components: component), options: [.noFollow]
)
guard attrs.fileType.isSymlink else {
// openat(2) failed, fstatat(2) succeeded, and it said it's not a symlink.
// If it's not a symlink, the error is not due to symlink following
// but other reasons, so just throw the error.
// e.g. open with O_DIRECTORY on a regular file.
throw openErrno
}

try self.symlink(component: component)
#endif
}
}
}

#if !os(Windows)
mutating func symlink(component: FilePath.Component) throws {
/// Thin wrapper around readlinkat(2)
func _readlinkat(_ fd: CInt, _ path: UnsafePointer<CChar>) throws -> FilePath {
var buffer = [CChar](repeating: 0, count: Int(PATH_MAX))
let length = try buffer.withUnsafeMutableBufferPointer { buffer in
try buffer.withMemoryRebound(to: Int8.self) { buffer in
guard let bufferBase = buffer.baseAddress else {
throw WASIAbi.Errno.EINVAL
}
return readlinkat(fd, path, bufferBase, buffer.count)
}
}
guard length >= 0 else {
throw try WASIAbi.Errno(platformErrno: errno)
}
return FilePath(String(cString: buffer))
}

guard resolvedSymlinks < Self.MAX_SYMLINKS else {
throw WASIAbi.Errno.ELOOP
}

// If it's a symlink, readlink(2) and check it doesn't escape sandbox.
let linkPath = try component.withPlatformString {
return try _readlinkat(self.baseFd.rawValue, $0)
}

guard !linkPath.isAbsolute else {
// Ban absolute symlink to avoid sandbox-escaping.
throw WASIAbi.Errno.EPERM
}

// Increment the number of resolved symlinks to prevent infinite
// link loop.
resolvedSymlinks += 1

// Add resolved path to the worklist.
self.components.append(contentsOf: linkPath.components.reversed())
}
#endif

mutating func resolve() throws -> FileDescriptor {
if path.isAbsolute {
// POSIX openat(2) interprets absolute path ignoring base directory fd
Expand Down
64 changes: 64 additions & 0 deletions Tests/WASITests/TestSupport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation

enum TestSupport {
struct Error: Swift.Error, CustomStringConvertible {
let description: String

init(description: String) {
self.description = description
}

init(errno: Int32) {
self.init(description: String(cString: strerror(errno)))
}
}

class TemporaryDirectory {
let path: String
var url: URL { URL(fileURLWithPath: path) }

init() throws {
let tempdir = URL(fileURLWithPath: NSTemporaryDirectory())
let templatePath = tempdir.appendingPathComponent("WasmKit.XXXXXX")
var template = [UInt8](templatePath.path.utf8).map({ Int8($0) }) + [Int8(0)]

#if os(Windows)
if _mktemp_s(&template, template.count) != 0 {
throw Error(errno: errno)
}
if _mkdir(template) != 0 {
throw Error(errno: errno)
}
#else
if mkdtemp(&template) == nil {
throw Error(errno: errno)
}
#endif

self.path = String(cString: template)
}

func createDir(at relativePath: String) throws {
let directoryURL = url.appendingPathComponent(relativePath)
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
}

func createFile(at relativePath: String, contents: String) throws {
let fileURL = url.appendingPathComponent(relativePath)
guard let data = contents.data(using: .utf8) else { return }
FileManager.default.createFile(atPath: fileURL.path, contents: data, attributes: nil)
}

func createSymlink(at relativePath: String, to target: String) throws {
let linkURL = url.appendingPathComponent(relativePath)
try FileManager.default.createSymbolicLink(
atPath: linkURL.path,
withDestinationPath: target
)
}

deinit {
_ = try? FileManager.default.removeItem(atPath: path)
}
}
}
Loading