1
1
import SystemExtras
2
2
import SystemPackage
3
3
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
+
4
19
struct PathResolution {
5
20
private let mode : FileDescriptor . AccessMode
6
21
private let options : FileDescriptor . OpenOptions
@@ -10,7 +25,20 @@ struct PathResolution {
10
25
private let path : FilePath
11
26
private var openDirectories : [ FileDescriptor ]
12
27
/// 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.
13
31
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
+ }
14
42
15
43
init (
16
44
baseDirFd: FileDescriptor ,
@@ -33,39 +61,117 @@ struct PathResolution {
33
61
// no more parent directory means too many `..`
34
62
throw WASIAbi . Errno. EPERM
35
63
}
64
+ try self . baseFd. close ( )
36
65
self . baseFd = lastDirectory
37
66
}
38
67
39
68
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
41
76
let mode : FileDescriptor . AccessMode
42
- if !self . components. isEmpty {
43
- var intermediateOptions : FileDescriptor . OpenOptions = [ ]
44
77
78
+ if !self . components. isEmpty {
45
79
#if !os(Windows)
46
80
// When trying to open an intermediate directory,
47
81
// 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)
51
83
#endif
52
- options = intermediateOptions
53
84
mode = . readOnly
54
85
} else {
55
- options = self . options
86
+ options. formUnion ( self . options)
56
87
mode = self . mode
57
88
}
58
89
59
90
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
+ }
66
130
}
67
131
}
68
132
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
+
69
175
mutating func resolve( ) throws -> FileDescriptor {
70
176
if path. isAbsolute {
71
177
// POSIX openat(2) interprets absolute path ignoring base directory fd
0 commit comments