11import SystemExtras
22import SystemPackage
33
4+ #if canImport(Darwin)
5+ import Darwin
6+ #elseif canImport(Glibc)
7+ import CSystem
8+ import Glibc
9+ #elseif canImport(Musl)
10+ import CSystem
11+ import Musl
12+ #elseif os(Windows)
13+ import CSystem
14+ import ucrt
15+ #else
16+ #error("Unsupported Platform")
17+ #endif
18+
419struct PathResolution {
520 private let mode : FileDescriptor . AccessMode
621 private let options : FileDescriptor . OpenOptions
@@ -10,7 +25,20 @@ struct PathResolution {
1025 private let path : FilePath
1126 private var openDirectories : [ FileDescriptor ]
1227 /// Reverse-ordered remaining path components
28+ /// File name appears first, then parent directories.
29+ /// e.g. `a/b/c` -> ["c", "b", "a"]
30+ /// This ordering is just to avoid dropFirst() on Array.
1331 private var components : FilePath . ComponentView
32+ private var resolvedSymlinks : Int = 0
33+
34+ private static var MAX_SYMLINKS : Int {
35+ // Linux defines MAXSYMLINKS as 40, but on darwin platforms, it's 32.
36+ // Take a single conservative value here to avoid platform-specific
37+ // behavior as much as possible.
38+ // * https://github.com/apple-oss-distributions/xnu/blob/8d741a5de7ff4191bf97d57b9f54c2f6d4a15585/bsd/sys/param.h#L207
39+ // * https://github.com/torvalds/linux/blob/850925a8133c73c4a2453c360b2c3beb3bab67c9/include/linux/namei.h#L13
40+ return 32
41+ }
1442
1543 init (
1644 baseDirFd: FileDescriptor ,
@@ -33,39 +61,117 @@ struct PathResolution {
3361 // no more parent directory means too many `..`
3462 throw WASIAbi . Errno. EPERM
3563 }
64+ try self . baseFd. close ( )
3665 self . baseFd = lastDirectory
3766 }
3867
3968 mutating func regular( component: FilePath . Component ) throws {
40- let options : FileDescriptor . OpenOptions
69+ var options : FileDescriptor . OpenOptions = [ ]
70+ #if !os(Windows)
71+ // First, try without following symlinks as a fast path.
72+ // If it's actually a symlink and options don't have O_NOFOLLOW,
73+ // we'll try again with interpreting resolved symlink.
74+ options. insert ( . noFollow)
75+ #endif
4176 let mode : FileDescriptor . AccessMode
42- if !self . components. isEmpty {
43- var intermediateOptions : FileDescriptor . OpenOptions = [ ]
4477
78+ if !self . components. isEmpty {
4579 #if !os(Windows)
4680 // When trying to open an intermediate directory,
4781 // we can assume it's directory.
48- intermediateOptions. insert ( . directory)
49- // FIXME: Resolve symlink in safe way
50- intermediateOptions. insert ( . noFollow)
82+ options. insert ( . directory)
5183 #endif
52- options = intermediateOptions
5384 mode = . readOnly
5485 } else {
55- options = self . options
86+ options. formUnion ( self . options)
5687 mode = self . mode
5788 }
5889
5990 try WASIAbi . Errno. translatingPlatformErrno {
60- let newFd = try self . baseFd. open (
61- at: FilePath ( root: nil , components: component) ,
62- mode, options: options, permissions: permissions
63- )
64- self . openDirectories. append ( self . baseFd)
65- self . baseFd = newFd
91+ do {
92+ let newFd = try self . baseFd. open (
93+ at: FilePath ( root: nil , components: component) ,
94+ mode, options: options, permissions: permissions
95+ )
96+ self . openDirectories. append ( self . baseFd)
97+ self . baseFd = newFd
98+ return
99+ } catch let openErrno as Errno {
100+ #if os(Windows)
101+ // Windows doesn't have O_NOFOLLOW, so we can't retry with following symlink.
102+ throw openErrno
103+ #else
104+ if self . options. contains ( . noFollow) {
105+ // If "open" failed with O_NOFOLLOW, no need to retry.
106+ throw openErrno
107+ }
108+
109+ // If "open" failed and it might be a symlink, try again with following symlink.
110+
111+ // Check if it's a symlink by fstatat(2).
112+ //
113+ // NOTE: `errno` has enough information to check if the component is a symlink,
114+ // but the value is platform-specific (e.g. ELOOP on POSIX standards, but EMLINK
115+ // on BSD family), so we conservatively check it by fstatat(2).
116+ let attrs = try self . baseFd. attributes (
117+ at: FilePath ( root: nil , components: component) , options: [ . noFollow]
118+ )
119+ guard attrs. fileType. isSymlink else {
120+ // openat(2) failed, fstatat(2) succeeded, and it said it's not a symlink.
121+ // If it's not a symlink, the error is not due to symlink following
122+ // but other reasons, so just throw the error.
123+ // e.g. open with O_DIRECTORY on a regular file.
124+ throw openErrno
125+ }
126+
127+ try self . symlink ( component: component)
128+ #endif
129+ }
66130 }
67131 }
68132
133+ #if !os(Windows)
134+ mutating func symlink( component: FilePath . Component ) throws {
135+ /// Thin wrapper around readlinkat(2)
136+ func _readlinkat( _ fd: CInt , _ path: UnsafePointer < CChar > ) throws -> FilePath {
137+ var buffer = [ CChar] ( repeating: 0 , count: Int ( PATH_MAX) )
138+ let length = try buffer. withUnsafeMutableBufferPointer { buffer in
139+ try buffer. withMemoryRebound ( to: Int8 . self) { buffer in
140+ guard let bufferBase = buffer. baseAddress else {
141+ throw WASIAbi . Errno. EINVAL
142+ }
143+ return readlinkat ( fd, path, bufferBase, buffer. count)
144+ }
145+ }
146+ guard length >= 0 else {
147+ throw try WASIAbi . Errno ( platformErrno: errno)
148+ }
149+ return FilePath ( String ( cString: buffer) )
150+ }
151+
152+ guard resolvedSymlinks < Self . MAX_SYMLINKS else {
153+ throw WASIAbi . Errno. ELOOP
154+ }
155+
156+ // If it's a symlink, readlink(2) and check it doesn't escape sandbox.
157+ let linkPath = try component. withPlatformString {
158+ return try _readlinkat ( self . baseFd. rawValue, $0)
159+ }
160+
161+ guard !linkPath. isAbsolute else {
162+ // Ban absolute symlink to avoid sandbox-escaping.
163+ throw WASIAbi . Errno. EPERM
164+ }
165+
166+ // Increment the number of resolved symlinks to prevent infinite
167+ // link loop.
168+ resolvedSymlinks += 1
169+
170+ // Add resolved path to the worklist.
171+ self . components. append ( contentsOf: linkPath. components. reversed ( ) )
172+ }
173+ #endif
174+
69175 mutating func resolve( ) throws -> FileDescriptor {
70176 if path. isAbsolute {
71177 // POSIX openat(2) interprets absolute path ignoring base directory fd
0 commit comments