File: Open.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (212 lines) | stat: -rw-r--r-- 7,972 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import SystemExtras
import SystemPackage

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

struct PathResolution {
    private let mode: FileDescriptor.AccessMode
    private let options: FileDescriptor.OpenOptions
    private let permissions: FilePermissions

    private var baseFd: FileDescriptor
    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,
        mode: FileDescriptor.AccessMode,
        options: FileDescriptor.OpenOptions,
        permissions: FilePermissions,
        path: FilePath
    ) {
        self.baseFd = baseDirFd
        self.mode = mode
        self.options = options
        self.permissions = permissions
        self.path = path
        self.openDirectories = []
        self.components = FilePath.ComponentView(path.components.reversed())
    }

    mutating func parentDirectory() throws {
        guard let lastDirectory = openDirectories.popLast() else {
            // 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 {
        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 {
            #if !os(Windows)
                // When trying to open an intermediate directory,
                // we can assume it's directory.
                options.insert(.directory)
            #endif
            mode = .readOnly
        } else {
            options.formUnion(self.options)
            mode = self.mode
        }

        try WASIAbi.Errno.translatingPlatformErrno {
            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
            // but it leads sandbox-escaping, so reject absolute path here.
            throw WASIAbi.Errno.EPERM
        }

        while let component = components.popLast() {
            switch component.kind {
            case .currentDirectory:
                break  // no-op
            case .parentDirectory:
                try parentDirectory()
            case .regular: try regular(component: component)
            }
        }
        return self.baseFd
    }
}

extension SandboxPrimitives {
    static func openAt(
        start startFd: FileDescriptor,
        path: FilePath,
        mode: FileDescriptor.AccessMode,
        options: FileDescriptor.OpenOptions,
        permissions: FilePermissions
    ) throws -> FileDescriptor {
        var resolution = PathResolution(
            baseDirFd: startFd, mode: mode, options: options,
            permissions: permissions, path: path
        )
        return try resolution.resolve()
    }
}