Coverage Report

Created: 2025-07-18 06:03

/rust/registry/src/index.crates.io-6f17d22bba15001f/cap-primitives-3.4.4/src/fs/symlink.rs
Line
Count
Source (jump to first uncovered line)
1
//! This defines `symlink`, the primary entrypoint to sandboxed symlink
2
//! creation.
3
4
use crate::fs::errors;
5
#[cfg(all(racy_asserts, not(windows)))]
6
use crate::fs::symlink_unchecked;
7
#[cfg(racy_asserts)]
8
use crate::fs::{canonicalize, manually, map_result, stat_unchecked, FollowSymlinks, Metadata};
9
#[cfg(all(racy_asserts, windows))]
10
use crate::fs::{symlink_dir_unchecked, symlink_file_unchecked};
11
use std::path::Path;
12
use std::{fs, io};
13
14
/// Perform a `symlinkat`-like operation, ensuring that the resolution of the
15
/// path never escapes the directory tree rooted at `start`. An error is
16
/// returned if the target path is absolute.
17
#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
18
#[cfg(not(windows))]
19
#[inline]
20
0
pub fn symlink(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> {
21
0
    // Don't allow creating symlinks to absolute paths. This isn't strictly
22
0
    // necessary to preserve the sandbox, since `open` will refuse to follow
23
0
    // absolute symlinks in any case. However, it is useful to enforce this
24
0
    // restriction so that a WASI program can't trick some other non-WASI
25
0
    // program into following an absolute path.
26
0
    if old_path.has_root() {
27
0
        return Err(errors::escape_attempt());
28
0
    }
29
0
30
0
    write_symlink_impl(old_path, new_start, new_path)
31
0
}
32
33
#[cfg(not(windows))]
34
0
fn write_symlink_impl(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> {
35
    use crate::fs::symlink_impl;
36
37
    #[cfg(racy_asserts)]
38
    let stat_before = stat_unchecked(new_start, new_path, FollowSymlinks::No);
39
40
    // Call the underlying implementation.
41
0
    let result = symlink_impl(old_path, new_start, new_path);
42
0
43
0
    #[cfg(racy_asserts)]
44
0
    let stat_after = stat_unchecked(new_start, new_path, FollowSymlinks::No);
45
0
46
0
    #[cfg(racy_asserts)]
47
0
    check_symlink(
48
0
        old_path,
49
0
        new_start,
50
0
        new_path,
51
0
        &stat_before,
52
0
        &result,
53
0
        &stat_after,
54
0
    );
55
0
56
0
    result
57
0
}
58
59
/// Perform a `symlinkat`-like operation, ensuring that the resolution of the
60
/// link path never escapes the directory tree rooted at `start`.
61
#[cfg(not(windows))]
62
0
pub fn symlink_contents<P: AsRef<Path>, Q: AsRef<Path>>(
63
0
    old_path: P,
64
0
    new_start: &fs::File,
65
0
    new_path: Q,
66
0
) -> io::Result<()> {
67
0
    write_symlink_impl(old_path.as_ref(), new_start, new_path.as_ref())
68
0
}
69
70
/// Perform a `symlink_file`-like operation, ensuring that the resolution of
71
/// the path never escapes the directory tree rooted at `start`.
72
#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
73
#[cfg(windows)]
74
#[inline]
75
pub fn symlink_file(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> {
76
    use crate::fs::symlink_file_impl;
77
78
    // As above, don't allow creating symlinks to absolute paths.
79
    if old_path.has_root() {
80
        return Err(errors::escape_attempt());
81
    }
82
83
    #[cfg(racy_asserts)]
84
    let stat_before = stat_unchecked(new_start, new_path, FollowSymlinks::No);
85
86
    // Call the underlying implementation.
87
    let result = symlink_file_impl(old_path, new_start, new_path);
88
89
    #[cfg(racy_asserts)]
90
    let stat_after = stat_unchecked(new_start, new_path, FollowSymlinks::No);
91
92
    #[cfg(racy_asserts)]
93
    check_symlink_file(
94
        old_path,
95
        new_start,
96
        new_path,
97
        &stat_before,
98
        &result,
99
        &stat_after,
100
    );
101
102
    result
103
}
104
105
/// Perform a `symlink_dir`-like operation, ensuring that the resolution of the
106
/// path never escapes the directory tree rooted at `start`.
107
#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
108
#[cfg(windows)]
109
#[inline]
110
pub fn symlink_dir(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> {
111
    use crate::fs::symlink_dir_impl;
112
113
    // As above, don't allow creating symlinks to absolute paths.
114
    if old_path.has_root() {
115
        return Err(errors::escape_attempt());
116
    }
117
118
    #[cfg(racy_asserts)]
119
    let stat_before = stat_unchecked(new_start, new_path, FollowSymlinks::No);
120
121
    // Call the underlying implementation.
122
    let result = symlink_dir_impl(old_path, new_start, new_path);
123
124
    #[cfg(racy_asserts)]
125
    let stat_after = stat_unchecked(new_start, new_path, FollowSymlinks::No);
126
127
    #[cfg(racy_asserts)]
128
    check_symlink_dir(
129
        old_path,
130
        new_start,
131
        new_path,
132
        &stat_before,
133
        &result,
134
        &stat_after,
135
    );
136
137
    result
138
}
139
140
#[cfg(all(not(windows), racy_asserts))]
141
#[allow(clippy::enum_glob_use)]
142
fn check_symlink(
143
    old_path: &Path,
144
    new_start: &fs::File,
145
    new_path: &Path,
146
    stat_before: &io::Result<Metadata>,
147
    result: &io::Result<()>,
148
    stat_after: &io::Result<Metadata>,
149
) {
150
    use io::ErrorKind::*;
151
152
    match (
153
        map_result(stat_before),
154
        map_result(result),
155
        map_result(stat_after),
156
    ) {
157
        (Err((NotFound, _)), Ok(()), Ok(metadata)) => {
158
            assert!(metadata.file_type().is_symlink());
159
            let canon =
160
                manually::canonicalize_with(new_start, new_path, FollowSymlinks::No).unwrap();
161
            assert_same_file_metadata!(
162
                &stat_unchecked(new_start, &canon, FollowSymlinks::No).unwrap(),
163
                &metadata
164
            );
165
        }
166
167
        (Ok(metadata_before), Err((AlreadyExists, _)), Ok(metadata_after)) => {
168
            assert_same_file_metadata!(&metadata_before, &metadata_after);
169
        }
170
171
        (_, Err((_kind, _message)), _) => match map_result(&canonicalize(new_start, new_path)) {
172
            Ok(canon) => match map_result(&symlink_unchecked(old_path, new_start, &canon)) {
173
                Err((_unchecked_kind, _unchecked_message)) => {
174
                    /* TODO: Check error messages.
175
                    assert_eq!(
176
                        kind,
177
                        unchecked_kind,
178
                        "unexpected error kind from symlink new_start='{:?}', \
179
                         new_path='{}':\nstat_before={:#?}\nresult={:#?}\nstat_after={:#?}",
180
                        new_start,
181
                        new_path.display(),
182
                        stat_before,
183
                        result,
184
                        stat_after
185
                    );
186
                    assert_eq!(message, unchecked_message);
187
                    */
188
                }
189
                _ => panic!("unsandboxed symlink success"),
190
            },
191
            Err((_canon_kind, _canon_message)) => {
192
                /* TODO: Check error messages.
193
                assert_eq!(kind, canon_kind);
194
                assert_eq!(message, canon_message);
195
                */
196
            }
197
        },
198
199
        _other => {
200
            /* TODO: Check error messages.
201
            panic!(
202
                "inconsistent symlink checks: new_start='{:?}' new_path='{}':\n{:#?}",
203
                new_start,
204
                new_path.display(),
205
                other,
206
            )
207
            */
208
        }
209
    }
210
}
211
212
#[cfg(all(windows, racy_asserts))]
213
#[allow(clippy::enum_glob_use)]
214
fn check_symlink_file(
215
    old_path: &Path,
216
    new_start: &fs::File,
217
    new_path: &Path,
218
    stat_before: &io::Result<Metadata>,
219
    result: &io::Result<()>,
220
    stat_after: &io::Result<Metadata>,
221
) {
222
    use io::ErrorKind::*;
223
224
    match (
225
        map_result(stat_before),
226
        map_result(result),
227
        map_result(stat_after),
228
    ) {
229
        (Err((NotFound, _)), Ok(()), Ok(metadata)) => {
230
            assert!(metadata.file_type().is_symlink());
231
            let canon =
232
                manually::canonicalize_with(new_start, new_path, FollowSymlinks::No).unwrap();
233
            assert_same_file_metadata!(
234
                &stat_unchecked(new_start, &canon, FollowSymlinks::No).unwrap(),
235
                &metadata
236
            );
237
        }
238
239
        (Ok(metadata_before), Err((AlreadyExists, _)), Ok(metadata_after)) => {
240
            assert_same_file_metadata!(&metadata_before, &metadata_after);
241
        }
242
243
        (_, Err((_kind, _message)), _) => match map_result(&canonicalize(new_start, new_path)) {
244
            Ok(canon) => match map_result(&symlink_file_unchecked(old_path, new_start, &canon)) {
245
                Err((_unchecked_kind, _unchecked_message)) => {
246
                    /* TODO: Check error messages.
247
                    assert_eq!(
248
                        kind,
249
                        unchecked_kind,
250
                        "unexpected error kind from symlink new_start='{:?}', \
251
                         new_path='{}':\nstat_before={:#?}\nresult={:#?}\nstat_after={:#?}",
252
                        new_start,
253
                        new_path.display(),
254
                        stat_before,
255
                        result,
256
                        stat_after
257
                    );
258
                    assert_eq!(message, unchecked_message);
259
                    */
260
                }
261
                _ => panic!("unsandboxed symlink success"),
262
            },
263
            Err((_canon_kind, _canon_message)) => {
264
                /* TODO: Check error messages.
265
                assert_eq!(kind, canon_kind);
266
                assert_eq!(message, canon_message);
267
                */
268
            }
269
        },
270
271
        _other => {
272
            /* TODO: Check error messages.
273
            panic!(
274
                "inconsistent symlink checks: new_start='{:?}' new_path='{}':\n{:#?}",
275
                new_start,
276
                new_path.display(),
277
                other,
278
            )
279
            */
280
        }
281
    }
282
}
283
284
#[cfg(all(windows, racy_asserts))]
285
#[allow(clippy::enum_glob_use)]
286
fn check_symlink_dir(
287
    old_path: &Path,
288
    new_start: &fs::File,
289
    new_path: &Path,
290
    stat_before: &io::Result<Metadata>,
291
    result: &io::Result<()>,
292
    stat_after: &io::Result<Metadata>,
293
) {
294
    use io::ErrorKind::*;
295
296
    match (
297
        map_result(stat_before),
298
        map_result(result),
299
        map_result(stat_after),
300
    ) {
301
        (Err((NotFound, _)), Ok(()), Ok(metadata)) => {
302
            assert!(metadata.file_type().is_symlink());
303
            let canon =
304
                manually::canonicalize_with(new_start, new_path, FollowSymlinks::No).unwrap();
305
            assert_same_file_metadata!(
306
                &stat_unchecked(new_start, &canon, FollowSymlinks::No).unwrap(),
307
                &metadata
308
            );
309
        }
310
311
        (Ok(metadata_before), Err((AlreadyExists, _)), Ok(metadata_after)) => {
312
            assert_same_file_metadata!(&metadata_before, &metadata_after);
313
        }
314
315
        (_, Err((_kind, _message)), _) => match map_result(&canonicalize(new_start, new_path)) {
316
            Ok(canon) => match map_result(&symlink_dir_unchecked(old_path, new_start, &canon)) {
317
                Err((_unchecked_kind, _unchecked_message)) => {
318
                    /* TODO: Check error messages.
319
                    assert_eq!(
320
                        kind,
321
                        unchecked_kind,
322
                        "unexpected error kind from symlink new_start='{:?}', \
323
                         new_path='{}':\nstat_before={:#?}\nresult={:#?}\nstat_after={:#?}",
324
                        new_start,
325
                        new_path.display(),
326
                        stat_before,
327
                        result,
328
                        stat_after
329
                    );
330
                    assert_eq!(message, unchecked_message);
331
                    */
332
                }
333
                _ => panic!("unsandboxed symlink success"),
334
            },
335
            Err((_canon_kind, _canon_message)) => {
336
                /* TODO: Check error messages.
337
                assert_eq!(kind, canon_kind);
338
                assert_eq!(message, canon_message);
339
                */
340
            }
341
        },
342
343
        _other => {
344
            /* TODO: Check error messages.
345
            panic!(
346
                "inconsistent symlink checks: new_start='{:?}' new_path='{}':\n{:#?}",
347
                new_start,
348
                new_path.display(),
349
                other,
350
            )
351
            */
352
        }
353
    }
354
}