1#![allow(
3 unsafe_code,
4 reason = "The no-follow openat primitive binds reads to a validated directory descriptor."
5)]
6
7use std::fs::File;
8use std::io;
9use std::path::Path;
10
11#[cfg(unix)]
12use std::io::Write;
13#[cfg(unix)]
14use std::os::fd::{AsRawFd, FromRawFd};
15
16#[cfg(unix)]
19pub fn open_directory_handle(path: &Path) -> io::Result<File> {
20 open_directory_beneath(path)
21}
22
23#[cfg(not(unix))]
24pub fn open_directory_handle(_path: &Path) -> io::Result<File> {
25 Err(io::Error::new(
26 io::ErrorKind::Unsupported,
27 "handle-bound directory operations are unavailable on this platform",
28 ))
29}
30
31#[cfg(unix)]
35pub fn set_command_working_directory(command: &mut std::process::Command, directory: &File) -> io::Result<()> {
36 use std::os::unix::process::CommandExt;
37
38 let descriptor = directory.as_raw_fd();
39 let change_directory = move || {
40 let result = unsafe { libc::fchdir(descriptor) };
43 if result == 0 {
44 Ok(())
45 } else {
46 Err(io::Error::last_os_error())
47 }
48 };
49 unsafe {
52 command.pre_exec(change_directory);
53 }
54 Ok(())
55}
56
57#[cfg(not(unix))]
58pub fn set_command_working_directory(_command: &mut std::process::Command, _directory: &File) -> io::Result<()> {
59 Err(io::Error::new(
60 io::ErrorKind::Unsupported,
61 "handle-bound process directories are unavailable on this platform",
62 ))
63}
64
65#[cfg(unix)]
68pub fn open_file_beneath(root: &Path, relative: &Path) -> io::Result<File> {
69 use std::ffi::CString;
70 use std::os::fd::{AsRawFd, FromRawFd};
71 use std::os::unix::ffi::OsStrExt;
72 use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
73 use std::path::Component;
74
75 let invalid =
76 || io::Error::new(io::ErrorKind::InvalidInput, "expected an absolute root and a normal relative file path");
77 if !root.is_absolute() || relative.as_os_str().is_empty() || relative.is_absolute() {
78 return Err(invalid());
79 }
80 let mut directory = std::fs::OpenOptions::new()
81 .read(true)
82 .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
83 .open("/")?;
84 let mut names = Vec::new();
85 for component in root.components() {
86 match component {
87 Component::RootDir => {}
88 Component::Normal(name) => names.push(name),
89 _ => return Err(invalid()),
90 }
91 }
92 for component in relative.components() {
93 match component {
94 Component::Normal(name) => names.push(name),
95 _ => return Err(invalid()),
96 }
97 }
98 let count = names.len();
99 for (index, name) in names.into_iter().enumerate() {
100 let name = CString::new(name.as_bytes()).map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
101 let last = index + 1 == count;
102 let flags = libc::O_RDONLY
103 | libc::O_NOFOLLOW
104 | libc::O_CLOEXEC
105 | if last { libc::O_NONBLOCK } else { libc::O_DIRECTORY };
106 let descriptor = unsafe { libc::openat(directory.as_raw_fd(), name.as_ptr(), flags) };
109 if descriptor < 0 {
110 return Err(io::Error::last_os_error());
111 }
112 directory = unsafe { File::from_raw_fd(descriptor) };
114 }
115 let metadata = directory.metadata()?;
116 if !metadata.is_file() || metadata.nlink() != 1 {
117 return Err(io::Error::new(io::ErrorKind::PermissionDenied, "expected a regular single-link file"));
118 }
119 Ok(directory)
120}
121
122#[cfg(unix)]
129pub fn ensure_directory_beneath(root: &Path, relative: &Path) -> io::Result<()> {
130 let mut directory = open_directory_beneath(root)?;
131 for component in normal_components(relative, false)? {
132 directory = open_or_create_directory_at(&directory, &component)?;
133 }
134 Ok(())
135}
136
137#[cfg(unix)]
140pub fn validate_directory_beneath(root: &Path, relative: &Path) -> io::Result<()> {
141 let mut directory = open_directory_beneath(root)?;
142 for component in normal_components(relative, false)? {
143 directory = open_directory_at(&directory, &component)?;
144 }
145 Ok(())
146}
147
148#[cfg(unix)]
152pub fn write_file_beneath(root: &Path, relative: &Path, contents: &[u8]) -> io::Result<()> {
153 let mut file = open_new_file_beneath(root, relative)?;
154 file.write_all(contents)?;
155 file.sync_all()
156}
157
158#[cfg(unix)]
161pub fn copy_file_beneath(
162 source_root: &Path,
163 source_relative: &Path,
164 destination_root: &Path,
165 destination_relative: &Path,
166) -> io::Result<()> {
167 let mut source = open_file_beneath(source_root, source_relative)?;
168 let mut destination = open_new_file_beneath(destination_root, destination_relative)?;
169 io::copy(&mut source, &mut destination)?;
170 destination.sync_all()
171}
172
173#[cfg(unix)]
176pub fn create_symlink_beneath(root: &Path, relative: &Path, target: &Path) -> io::Result<()> {
177 use std::os::unix::ffi::OsStrExt;
178
179 let components = normal_components(relative, false)?;
180 let (file_name, parent_components) = components
181 .split_last()
182 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "expected a relative symlink path"))?;
183 let mut directory = open_directory_beneath(root)?;
184 for component in parent_components {
185 directory = open_or_create_directory_at(&directory, component)?;
186 }
187
188 let name = c_string(file_name)?;
189 let target = std::ffi::CString::new(target.as_os_str().as_bytes())
190 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
191 let result = unsafe { libc::symlinkat(target.as_ptr(), directory.as_raw_fd(), name.as_ptr()) };
194 if result < 0 {
195 return Err(io::Error::last_os_error());
196 }
197 Ok(())
198}
199
200#[cfg(unix)]
204pub fn open_lock_file_beneath(root: &Path, relative: &Path) -> io::Result<File> {
205 use std::os::unix::fs::MetadataExt;
206
207 let components = normal_components(relative, false)?;
208 let (file_name, parent_components) = components
209 .split_last()
210 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "expected a relative lock file path"))?;
211 let mut directory = open_directory_beneath(root)?;
212 for component in parent_components {
213 directory = open_directory_at(&directory, component)?;
214 }
215
216 let name = c_string(file_name)?;
217 let descriptor = unsafe {
221 libc::openat(
222 directory.as_raw_fd(),
223 name.as_ptr(),
224 libc::O_RDWR | libc::O_CREAT | libc::O_NOFOLLOW | libc::O_CLOEXEC,
225 0o600,
226 )
227 };
228 if descriptor < 0 {
229 return Err(io::Error::last_os_error());
230 }
231 let file = unsafe { File::from_raw_fd(descriptor) };
233 let metadata = file.metadata()?;
234 if !metadata.is_file() || metadata.nlink() != 1 {
235 return Err(io::Error::new(io::ErrorKind::PermissionDenied, "expected a regular single-link lock file"));
236 }
237 Ok(file)
238}
239
240#[cfg(not(unix))]
241pub fn ensure_directory_beneath(root: &Path, relative: &Path) -> io::Result<()> {
242 let components = normal_components(relative, false)?;
243 let mut current = root.to_path_buf();
244 let root_metadata = std::fs::symlink_metadata(¤t)?;
245 if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
246 return Err(io::Error::other("trusted root must be a regular directory"));
247 }
248 for component in components {
249 current.push(component);
250 match std::fs::symlink_metadata(¤t) {
251 Ok(metadata) if metadata.file_type().is_symlink() => {
252 return Err(io::Error::other(format!("refusing symlink directory {}", current.display())));
253 }
254 Ok(metadata) if !metadata.is_dir() => {
255 return Err(io::Error::other(format!("{} is not a directory", current.display())));
256 }
257 Ok(_) => {}
258 Err(error) if error.kind() == io::ErrorKind::NotFound => {
259 std::fs::create_dir(¤t)?;
260 }
261 Err(error) => return Err(error),
262 }
263 }
264 Ok(())
265}
266
267#[cfg(not(unix))]
268pub fn validate_directory_beneath(root: &Path, relative: &Path) -> io::Result<()> {
269 let components = normal_components(relative, false)?;
270 let mut current = root.to_path_buf();
271 let root_metadata = std::fs::symlink_metadata(¤t)?;
272 if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
273 return Err(io::Error::other("trusted root must be a regular directory"));
274 }
275 for component in components {
276 current.push(component);
277 let metadata = std::fs::symlink_metadata(¤t)?;
278 if metadata.file_type().is_symlink() || !metadata.is_dir() {
279 return Err(io::Error::other(format!("{} is not a regular directory", current.display())));
280 }
281 }
282 Ok(())
283}
284
285#[cfg(not(unix))]
286pub fn write_file_beneath(root: &Path, relative: &Path, contents: &[u8]) -> io::Result<()> {
287 let _ = (root, relative, contents);
288 Err(io::Error::new(
289 io::ErrorKind::Unsupported,
290 "handle-bound file writes are unavailable on this platform",
291 ))
292}
293
294#[cfg(not(unix))]
295pub fn copy_file_beneath(
296 _source_root: &Path,
297 _source_relative: &Path,
298 _destination_root: &Path,
299 _destination_relative: &Path,
300) -> io::Result<()> {
301 Err(io::Error::new(
302 io::ErrorKind::Unsupported,
303 "handle-bound file copies are unavailable on this platform",
304 ))
305}
306
307#[cfg(not(unix))]
308pub fn create_symlink_beneath(_root: &Path, _relative: &Path, _target: &Path) -> io::Result<()> {
309 Err(io::Error::new(
310 io::ErrorKind::Unsupported,
311 "handle-bound symlink creation is unavailable on this platform",
312 ))
313}
314
315#[cfg(not(unix))]
318pub fn open_lock_file_beneath(root: &Path, relative: &Path) -> io::Result<File> {
319 use std::fs::OpenOptions;
320
321 let components = normal_components(relative, false)?;
322 let (file_name, parent_components) = components
323 .split_last()
324 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "expected a relative lock file path"))?;
325 let parent = parent_components.iter().fold(root.to_path_buf(), |mut path, component| {
326 path.push(component);
327 path
328 });
329 let relative_parent = parent
330 .strip_prefix(root)
331 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bound path escaped trusted root"))?;
332 ensure_directory_beneath(root, relative_parent)?;
333 let path = parent.join(file_name);
334 if let Ok(metadata) = std::fs::symlink_metadata(&path)
335 && (metadata.file_type().is_symlink() || !metadata.is_file())
336 {
337 return Err(io::Error::new(io::ErrorKind::PermissionDenied, "expected a regular lock file"));
338 }
339 let file = OpenOptions::new().read(true).write(true).create(true).open(&path)?;
340 let metadata = std::fs::symlink_metadata(&path)?;
341 if metadata.file_type().is_symlink() || !metadata.is_file() {
342 return Err(io::Error::new(io::ErrorKind::PermissionDenied, "expected a regular lock file"));
343 }
344 Ok(file)
345}
346
347#[cfg(unix)]
348fn open_directory_beneath(root: &Path) -> io::Result<File> {
349 let mut directory = open_directory_at_path(Path::new("/"))?;
350 for component in normal_components(root, true)? {
351 directory = open_directory_at(&directory, &component)?;
352 }
353 Ok(directory)
354}
355
356#[cfg(unix)]
357fn open_new_file_beneath(root: &Path, relative: &Path) -> io::Result<File> {
358 let components = normal_components(relative, false)?;
359 let (file_name, parent_components) = components
360 .split_last()
361 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "expected a relative file path"))?;
362 let mut directory = open_directory_beneath(root)?;
363 for component in parent_components {
364 directory = open_or_create_directory_at(&directory, component)?;
365 }
366
367 let name = c_string(file_name)?;
368 let flags = libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW | libc::O_CLOEXEC;
369 let descriptor = unsafe { libc::openat(directory.as_raw_fd(), name.as_ptr(), flags, 0o600) };
372 if descriptor < 0 {
373 return Err(io::Error::last_os_error());
374 }
375 Ok(unsafe { File::from_raw_fd(descriptor) })
377}
378
379#[cfg(unix)]
380fn open_directory_at_path(path: &Path) -> io::Result<File> {
381 use std::os::unix::fs::OpenOptionsExt;
382
383 std::fs::OpenOptions::new()
384 .read(true)
385 .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
386 .open(path)
387}
388
389#[cfg(unix)]
390fn open_directory_at(parent: &File, component: &std::ffi::OsString) -> io::Result<File> {
391 let name = c_string(component)?;
392 let descriptor = unsafe {
395 libc::openat(
396 parent.as_raw_fd(),
397 name.as_ptr(),
398 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
399 )
400 };
401 if descriptor < 0 {
402 return Err(io::Error::last_os_error());
403 }
404 Ok(unsafe { File::from_raw_fd(descriptor) })
406}
407
408#[cfg(unix)]
409fn open_or_create_directory_at(parent: &File, component: &std::ffi::OsString) -> io::Result<File> {
410 match open_directory_at(parent, component) {
411 Ok(directory) => Ok(directory),
412 Err(error) if error.kind() == io::ErrorKind::NotFound => {
413 let name = c_string(component)?;
414 let result = unsafe { libc::mkdirat(parent.as_raw_fd(), name.as_ptr(), 0o700) };
417 if result < 0 {
418 let mkdir_error = io::Error::last_os_error();
419 if mkdir_error.kind() != io::ErrorKind::AlreadyExists {
420 return Err(mkdir_error);
421 }
422 }
423 open_directory_at(parent, component)
424 }
425 Err(error) => Err(error),
426 }
427}
428
429fn normal_components(path: &Path, absolute: bool) -> io::Result<Vec<std::ffi::OsString>> {
430 use std::path::Component;
431
432 if absolute != path.is_absolute() || (!absolute && path.as_os_str().is_empty()) {
433 return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid bound path"));
434 }
435 path.components()
436 .filter_map(|component| match component {
437 Component::Prefix(_) if absolute => None,
438 Component::RootDir if absolute => None,
439 Component::Normal(name) => Some(Ok(name.to_os_string())),
440 _ => Some(Err(io::Error::new(io::ErrorKind::InvalidInput, "bound path contains traversal"))),
441 })
442 .collect()
443}
444
445#[cfg(unix)]
446fn c_string(name: &std::ffi::OsString) -> io::Result<std::ffi::CString> {
447 use std::os::unix::ffi::OsStrExt;
448
449 std::ffi::CString::new(name.as_os_str().as_bytes())
450 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))
451}
452
453#[cfg(not(unix))]
454pub fn open_file_beneath(_root: &Path, _relative: &Path) -> io::Result<File> {
455 Err(io::Error::new(io::ErrorKind::Unsupported, "bound no-follow reads are unavailable on this platform"))
456}
457
458#[cfg(all(test, unix))]
459mod tests {
460 use super::*;
461 use std::io::Read;
462 use std::os::unix::fs::symlink;
463
464 #[test]
465 fn bound_read_rejects_parent_final_links_and_traversal() {
466 let temp = tempfile::tempdir().unwrap();
467 let root = crate::canonicalize(temp.path()).unwrap();
468 std::fs::create_dir(root.join("real")).unwrap();
469 std::fs::write(root.join("real/output"), "retained output").unwrap();
470 symlink(root.join("real"), root.join("alias")).unwrap();
471 symlink(root.join("real/output"), root.join("link")).unwrap();
472 assert!(open_file_beneath(&root, Path::new("alias/output")).is_err());
473 assert!(open_file_beneath(&root, Path::new("link")).is_err());
474 assert!(open_file_beneath(&root, Path::new("../outside")).is_err());
475 assert!(open_file_beneath(&root, Path::new("real")).is_err());
476 let mut opened = open_file_beneath(&root, Path::new("real/output")).unwrap();
477 std::fs::rename(root.join("real"), root.join("retained")).unwrap();
478 symlink("/", root.join("real")).unwrap();
479 let mut text = String::new();
480 opened.read_to_string(&mut text).unwrap();
481 assert_eq!(text, "retained output");
482 }
483}