cap_primitives/fs/
symlink.rs

1//! This defines `symlink`, the primary entrypoint to sandboxed symlink
2//! creation.
3
4use crate::fs::errors;
5#[cfg(all(racy_asserts, not(windows)))]
6use crate::fs::symlink_unchecked;
7#[cfg(racy_asserts)]
8use crate::fs::{canonicalize, manually, map_result, stat_unchecked, FollowSymlinks, Metadata};
9#[cfg(all(racy_asserts, windows))]
10use crate::fs::{symlink_dir_unchecked, symlink_file_unchecked};
11use std::path::Path;
12use 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]
20pub fn symlink(old_path: &Path, new_start: &fs::File, new_path: &Path) -> io::Result<()> {
21    // Don't allow creating symlinks to absolute paths. This isn't strictly
22    // necessary to preserve the sandbox, since `open` will refuse to follow
23    // absolute symlinks in any case. However, it is useful to enforce this
24    // restriction so that a WASI program can't trick some other non-WASI
25    // program into following an absolute path.
26    if old_path.has_root() {
27        return Err(errors::escape_attempt());
28    }
29
30    write_symlink_impl(old_path, new_start, new_path)
31}
32
33#[cfg(not(windows))]
34fn 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    let result = symlink_impl(old_path, new_start, new_path);
42
43    #[cfg(racy_asserts)]
44    let stat_after = stat_unchecked(new_start, new_path, FollowSymlinks::No);
45
46    #[cfg(racy_asserts)]
47    check_symlink(
48        old_path,
49        new_start,
50        new_path,
51        &stat_before,
52        &result,
53        &stat_after,
54    );
55
56    result
57}
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))]
62pub fn symlink_contents<P: AsRef<Path>, Q: AsRef<Path>>(
63    old_path: P,
64    new_start: &fs::File,
65    new_path: Q,
66) -> io::Result<()> {
67    write_symlink_impl(old_path.as_ref(), new_start, new_path.as_ref())
68}
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]
75pub 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]
110pub 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)]
142fn 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)]
214fn 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)]
286fn 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}