Coverage Report

Created: 2025-07-18 06:03

/rust/registry/src/index.crates.io-6f17d22bba15001f/rustix-linux-procfs-0.1.1/src/lib.rs
Line
Count
Source (jump to first uncovered line)
1
#![doc = include_str!("../README.md")]
2
#![cfg(any(target_os = "linux", target_os = "android"))]
3
#![no_std]
4
5
use core::mem::MaybeUninit;
6
use once_cell::sync::OnceCell;
7
use rustix::cstr;
8
use rustix::fd::{AsFd, BorrowedFd, OwnedFd};
9
use rustix::ffi::CStr;
10
use rustix::fs::{
11
    fstat, fstatfs, major, openat, readlinkat_raw, renameat, seek, FileType, FsWord, Mode, OFlags,
12
    RawDir, SeekFrom, Stat, CWD, PROC_SUPER_MAGIC,
13
};
14
use rustix::io;
15
use rustix::path::DecInt;
16
17
/// Linux's procfs always uses inode 1 for its root directory.
18
const PROC_ROOT_INO: u64 = 1;
19
20
// Identify an entry within "/proc", to determine which anomalies to check for.
21
#[derive(Copy, Clone, Debug)]
22
enum Kind {
23
    Proc,
24
    Pid,
25
    Fd,
26
    File,
27
    Symlink,
28
}
29
30
/// Check a subdirectory of "/proc" for anomalies.
31
0
fn check_proc_entry(
32
0
    kind: Kind,
33
0
    entry: BorrowedFd<'_>,
34
0
    proc_stat: Option<&Stat>,
35
0
) -> io::Result<Stat> {
36
0
    let entry_stat = fstat(entry)?;
37
0
    check_proc_entry_with_stat(kind, entry, entry_stat, proc_stat)
38
0
}
39
40
/// Check a subdirectory of "/proc" for anomalies, using the provided `Stat`.
41
0
fn check_proc_entry_with_stat(
42
0
    kind: Kind,
43
0
    entry: BorrowedFd<'_>,
44
0
    entry_stat: Stat,
45
0
    proc_stat: Option<&Stat>,
46
0
) -> io::Result<Stat> {
47
0
    // Check the filesystem magic.
48
0
    check_procfs(entry)?;
49
50
0
    match kind {
51
0
        Kind::Proc => check_proc_root(entry, &entry_stat)?,
52
0
        Kind::Pid | Kind::Fd => check_proc_subdir(entry, &entry_stat, proc_stat)?,
53
0
        Kind::File => check_proc_file(&entry_stat, proc_stat)?,
54
0
        Kind::Symlink => check_proc_symlink(&entry_stat, proc_stat)?,
55
    }
56
57
    // "/proc" directories are typically mounted r-xr-xr-x.
58
    // "/proc/self/fd" is r-x------. Allow them to have fewer permissions, but
59
    // not more.
60
0
    match kind {
61
0
        Kind::Symlink => {
62
0
            // On Linux, symlinks don't have their own permissions.
63
0
        }
64
        _ => {
65
0
            let expected_mode = if let Kind::Fd = kind { 0o500 } else { 0o555 };
66
0
            if entry_stat.st_mode & 0o777 & !expected_mode != 0 {
67
0
                return Err(io::Errno::NOTSUP);
68
0
            }
69
        }
70
    }
71
72
0
    match kind {
73
        Kind::Fd => {
74
            // Check that the "/proc/self/fd" directory doesn't have any
75
            // extraneous links into it (which might include unexpected
76
            // subdirectories).
77
0
            if entry_stat.st_nlink != 2 {
78
0
                return Err(io::Errno::NOTSUP);
79
0
            }
80
        }
81
        Kind::Pid | Kind::Proc => {
82
            // Check that the "/proc" and "/proc/self" directories aren't
83
            // empty.
84
0
            if entry_stat.st_nlink <= 2 {
85
0
                return Err(io::Errno::NOTSUP);
86
0
            }
87
        }
88
        Kind::File => {
89
            // Check that files in procfs don't have extraneous hard links to
90
            // them (which might indicate hard links to other things).
91
0
            if entry_stat.st_nlink != 1 {
92
0
                return Err(io::Errno::NOTSUP);
93
0
            }
94
        }
95
        Kind::Symlink => {
96
            // Check that symlinks in procfs don't have extraneous hard links
97
            // to them (which might indicate hard links to other things).
98
0
            if entry_stat.st_nlink != 1 {
99
0
                return Err(io::Errno::NOTSUP);
100
0
            }
101
        }
102
    }
103
104
0
    Ok(entry_stat)
105
0
}
106
107
0
fn check_proc_root(entry: BorrowedFd<'_>, stat: &Stat) -> io::Result<()> {
108
0
    // We use `O_DIRECTORY` for proc directories, so open should fail if we
109
0
    // don't get a directory when we expect one.
110
0
    assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
111
112
    // Check the root inode number.
113
0
    if stat.st_ino != PROC_ROOT_INO {
114
0
        return Err(io::Errno::NOTSUP);
115
0
    }
116
0
117
0
    // Proc is a non-device filesystem, so check for major number 0.
118
0
    // <https://www.kernel.org/doc/Documentation/admin-guide/devices.txt>
119
0
    if major(stat.st_dev) != 0 {
120
0
        return Err(io::Errno::NOTSUP);
121
0
    }
122
0
123
0
    // Check that "/proc" is a mountpoint.
124
0
    if !is_mountpoint(entry) {
125
0
        return Err(io::Errno::NOTSUP);
126
0
    }
127
0
128
0
    Ok(())
129
0
}
130
131
0
fn check_proc_subdir(
132
0
    entry: BorrowedFd<'_>,
133
0
    stat: &Stat,
134
0
    proc_stat: Option<&Stat>,
135
0
) -> io::Result<()> {
136
0
    // We use `O_DIRECTORY` for proc directories, so open should fail if we
137
0
    // don't get a directory when we expect one.
138
0
    assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
139
140
0
    check_proc_nonroot(stat, proc_stat)?;
141
142
    // Check that subdirectories of "/proc" are not mount points.
143
0
    if is_mountpoint(entry) {
144
0
        return Err(io::Errno::NOTSUP);
145
0
    }
146
0
147
0
    Ok(())
148
0
}
149
150
0
fn check_proc_file(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> {
151
0
    // Check that we have a regular file.
152
0
    if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile {
153
0
        return Err(io::Errno::NOTSUP);
154
0
    }
155
0
156
0
    check_proc_nonroot(stat, proc_stat)?;
157
158
0
    Ok(())
159
0
}
160
161
0
fn check_proc_symlink(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> {
162
0
    // Check that we have a symbolic link.
163
0
    if FileType::from_raw_mode(stat.st_mode) != FileType::Symlink {
164
0
        return Err(io::Errno::NOTSUP);
165
0
    }
166
0
167
0
    check_proc_nonroot(stat, proc_stat)?;
168
169
0
    Ok(())
170
0
}
171
172
0
fn check_proc_nonroot(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> {
173
0
    // Check that we haven't been linked back to the root of "/proc".
174
0
    if stat.st_ino == PROC_ROOT_INO {
175
0
        return Err(io::Errno::NOTSUP);
176
0
    }
177
0
178
0
    // Check that we're still in procfs.
179
0
    if stat.st_dev != proc_stat.unwrap().st_dev {
180
0
        return Err(io::Errno::NOTSUP);
181
0
    }
182
0
183
0
    Ok(())
184
0
}
185
186
/// Check that `file` is opened on a `procfs` filesystem.
187
0
fn check_procfs(file: BorrowedFd<'_>) -> io::Result<()> {
188
0
    let statfs = fstatfs(file)?;
189
0
    let f_type = statfs.f_type;
190
0
    if f_type != FsWord::from(PROC_SUPER_MAGIC) {
191
0
        return Err(io::Errno::NOTSUP);
192
0
    }
193
0
194
0
    Ok(())
195
0
}
196
197
/// Check whether the given directory handle is a mount point.
198
0
fn is_mountpoint(file: BorrowedFd<'_>) -> bool {
199
    // We use a `renameat` call that would otherwise fail, but which fails with
200
    // `XDEV` first if it would cross a mount point.
201
0
    let err = renameat(file, cstr!("../."), file, cstr!(".")).unwrap_err();
202
0
    match err {
203
0
        io::Errno::XDEV => true,  // the rename failed due to crossing a mount point
204
0
        io::Errno::BUSY => false, // the rename failed normally
205
0
        _ => panic!("Unexpected error from `renameat`: {:?}", err),
206
    }
207
0
}
208
209
/// Open a directory in `/proc`, mapping all errors to `io::Errno::NOTSUP`.
210
0
fn proc_opendirat<P: rustix::path::Arg, Fd: AsFd>(dirfd: Fd, path: P) -> io::Result<OwnedFd> {
211
0
    // We don't add `PATH` here because that disables `DIRECTORY`. And we don't
212
0
    // add `NOATIME` for the same reason as the comment in `open_and_check_file`.
213
0
    let oflags =
214
0
        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOCTTY;
215
0
    openat(dirfd, path, oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP)
Unexecuted instantiation: rustix_linux_procfs::proc_opendirat::<&core::ffi::c_str::CStr, std::os::fd::owned::BorrowedFd>::{closure#0}
Unexecuted instantiation: rustix_linux_procfs::proc_opendirat::<&[u8], std::os::fd::owned::BorrowedFd>::{closure#0}
216
0
}
Unexecuted instantiation: rustix_linux_procfs::proc_opendirat::<&core::ffi::c_str::CStr, std::os::fd::owned::BorrowedFd>
Unexecuted instantiation: rustix_linux_procfs::proc_opendirat::<&[u8], std::os::fd::owned::BorrowedFd>
217
218
/// Returns a handle to Linux's `/proc` directory.
219
///
220
/// This ensures that `/proc` is procfs, that nothing is mounted on top of it,
221
/// and that it looks normal. It also returns the `Stat` of `/proc`.
222
///
223
/// # References
224
///  - [Linux]
225
///
226
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
227
0
fn proc() -> io::Result<(BorrowedFd<'static>, &'static Stat)> {
228
    static PROC: StaticFd = StaticFd::new();
229
230
    // `OnceBox` is “racy” in that the initialization function may run
231
    // multiple times. We're ok with that, since the initialization function
232
    // has no side effects.
233
0
    PROC.get_or_try_init(|| {
234
        // Open "/proc".
235
0
        let proc = proc_opendirat(CWD, cstr!("/proc"))?;
236
0
        let proc_stat =
237
0
            check_proc_entry(Kind::Proc, proc.as_fd(), None).map_err(|_err| io::Errno::NOTSUP)?;
238
239
0
        Ok(new_static_fd(proc, proc_stat))
240
0
    })
241
0
    .map(|(fd, stat)| (fd.as_fd(), stat))
242
0
}
243
244
/// Returns a handle to Linux's `/proc/self` directory.
245
///
246
/// This ensures that `/proc/self` is procfs, that nothing is mounted on top of
247
/// it, and that it looks normal. It also returns the `Stat` of `/proc/self`.
248
///
249
/// # References
250
///  - [Linux]
251
///
252
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
253
#[allow(unsafe_code)]
254
0
fn proc_self() -> io::Result<(BorrowedFd<'static>, &'static Stat)> {
255
    static PROC_SELF: StaticFd = StaticFd::new();
256
257
    // The init function here may run multiple times; see above.
258
0
    PROC_SELF
259
0
        .get_or_try_init(|| {
260
0
            let (proc, proc_stat) = proc()?;
261
262
            // `getpid` would return our pid in our own pid namespace, so
263
            // instead use `readlink` on the `self` symlink to learn our pid in
264
            // the procfs namespace.
265
0
            let self_symlink = open_and_check_file(proc, proc_stat, cstr!("self"), Kind::Symlink)?;
266
0
            let mut buf = [MaybeUninit::<u8>::uninit(); 20];
267
0
            let (init, _uninit) = readlinkat_raw(self_symlink, cstr!(""), &mut buf)?;
268
0
            let pid: &[u8] = unsafe { core::mem::transmute(init) };
269
270
            // Open "/proc/self". Use our pid to compute the name rather than
271
            // literally using "self", as "self" is a symlink.
272
0
            let proc_self = proc_opendirat(proc, pid)?;
273
0
            let proc_self_stat = check_proc_entry(Kind::Pid, proc_self.as_fd(), Some(proc_stat))
274
0
                .map_err(|_err| io::Errno::NOTSUP)?;
275
276
0
            Ok(new_static_fd(proc_self, proc_self_stat))
277
0
        })
278
0
        .map(|(owned, stat)| (owned.as_fd(), stat))
279
0
}
280
281
/// Returns a handle to Linux's `/proc/self/fd` directory.
282
///
283
/// This ensures that `/proc/self/fd` is `procfs`, that nothing is mounted on
284
/// top of it, and that it looks normal.
285
///
286
/// # References
287
///  - [Linux]
288
///
289
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
290
#[cfg_attr(docsrs, doc(cfg(feature = "procfs")))]
291
0
pub fn proc_self_fd() -> io::Result<BorrowedFd<'static>> {
292
    static PROC_SELF_FD: StaticFd = StaticFd::new();
293
294
    // The init function here may run multiple times; see above.
295
0
    PROC_SELF_FD
296
0
        .get_or_try_init(|| {
297
0
            let (_, proc_stat) = proc()?;
298
299
0
            let (proc_self, _proc_self_stat) = proc_self()?;
300
301
            // Open "/proc/self/fd".
302
0
            let proc_self_fd = proc_opendirat(proc_self, cstr!("fd"))?;
303
0
            let proc_self_fd_stat =
304
0
                check_proc_entry(Kind::Fd, proc_self_fd.as_fd(), Some(proc_stat))
305
0
                    .map_err(|_err| io::Errno::NOTSUP)?;
306
307
0
            Ok(new_static_fd(proc_self_fd, proc_self_fd_stat))
308
0
        })
309
0
        .map(|(owned, _stat)| owned.as_fd())
310
0
}
311
312
type StaticFd = OnceCell<(OwnedFd, Stat)>;
313
314
#[inline]
315
0
fn new_static_fd(fd: OwnedFd, stat: Stat) -> (OwnedFd, Stat) {
316
0
    (fd, stat)
317
0
}
318
319
/// Returns a handle to Linux's `/proc/self/fdinfo` directory.
320
///
321
/// This ensures that `/proc/self/fdinfo` is `procfs`, that nothing is mounted
322
/// on top of it, and that it looks normal. It also returns the `Stat` of
323
/// `/proc/self/fd`.
324
///
325
/// # References
326
///  - [Linux]
327
///
328
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
329
0
fn proc_self_fdinfo() -> io::Result<(BorrowedFd<'static>, &'static Stat)> {
330
    static PROC_SELF_FDINFO: StaticFd = StaticFd::new();
331
332
0
    PROC_SELF_FDINFO
333
0
        .get_or_try_init(|| {
334
0
            let (_, proc_stat) = proc()?;
335
336
0
            let (proc_self, _proc_self_stat) = proc_self()?;
337
338
            // Open "/proc/self/fdinfo".
339
0
            let proc_self_fdinfo = proc_opendirat(proc_self, cstr!("fdinfo"))?;
340
0
            let proc_self_fdinfo_stat =
341
0
                check_proc_entry(Kind::Fd, proc_self_fdinfo.as_fd(), Some(proc_stat))
342
0
                    .map_err(|_err| io::Errno::NOTSUP)?;
343
344
0
            Ok((proc_self_fdinfo, proc_self_fdinfo_stat))
345
0
        })
346
0
        .map(|(owned, stat)| (owned.as_fd(), stat))
347
0
}
348
349
/// Returns a handle to a Linux `/proc/self/fdinfo/<fd>` file.
350
///
351
/// This ensures that `/proc/self/fdinfo/<fd>` is `procfs`, that nothing is
352
/// mounted on top of it, and that it looks normal.
353
///
354
/// # References
355
///  - [Linux]
356
///
357
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
358
#[inline]
359
#[cfg_attr(docsrs, doc(cfg(feature = "procfs")))]
360
0
pub fn proc_self_fdinfo_fd<Fd: AsFd>(fd: Fd) -> io::Result<OwnedFd> {
361
0
    _proc_self_fdinfo(fd.as_fd())
362
0
}
363
364
0
fn _proc_self_fdinfo(fd: BorrowedFd<'_>) -> io::Result<OwnedFd> {
365
0
    let (proc_self_fdinfo, proc_self_fdinfo_stat) = proc_self_fdinfo()?;
366
0
    let fd_str = DecInt::from_fd(fd);
367
0
    open_and_check_file(
368
0
        proc_self_fdinfo,
369
0
        proc_self_fdinfo_stat,
370
0
        fd_str.as_c_str(),
371
0
        Kind::File,
372
0
    )
373
0
}
374
375
/// Returns a handle to a Linux `/proc/self/pagemap` file.
376
///
377
/// This ensures that `/proc/self/pagemap` is `procfs`, that nothing is
378
/// mounted on top of it, and that it looks normal.
379
///
380
/// # References
381
///  - [Linux]
382
///  - [Linux pagemap]
383
///
384
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
385
/// [Linux pagemap]: https://www.kernel.org/doc/Documentation/vm/pagemap.txt
386
#[inline]
387
#[cfg_attr(docsrs, doc(cfg(feature = "procfs")))]
388
0
pub fn proc_self_pagemap() -> io::Result<OwnedFd> {
389
0
    proc_self_file(cstr!("pagemap"))
390
0
}
391
392
/// Returns a handle to a Linux `/proc/self/maps` file.
393
///
394
/// This ensures that `/proc/self/maps` is `procfs`, that nothing is
395
/// mounted on top of it, and that it looks normal.
396
///
397
/// # References
398
///  - [Linux]
399
///
400
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
401
#[inline]
402
#[cfg_attr(docsrs, doc(cfg(feature = "procfs")))]
403
0
pub fn proc_self_maps() -> io::Result<OwnedFd> {
404
0
    proc_self_file(cstr!("maps"))
405
0
}
406
407
/// Returns a handle to a Linux `/proc/self/status` file.
408
///
409
/// This ensures that `/proc/self/status` is `procfs`, that nothing is
410
/// mounted on top of it, and that it looks normal.
411
///
412
/// # References
413
///  - [Linux]
414
///
415
/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
416
#[inline]
417
#[cfg_attr(docsrs, doc(cfg(feature = "procfs")))]
418
0
pub fn proc_self_status() -> io::Result<OwnedFd> {
419
0
    proc_self_file(cstr!("status"))
420
0
}
421
422
/// Open a file under `/proc/self`.
423
0
fn proc_self_file(name: &CStr) -> io::Result<OwnedFd> {
424
0
    let (proc_self, proc_self_stat) = proc_self()?;
425
0
    open_and_check_file(proc_self, proc_self_stat, name, Kind::File)
426
0
}
427
428
/// Open a procfs file within in `dir` and check it for bind mounts.
429
0
fn open_and_check_file(
430
0
    dir: BorrowedFd<'_>,
431
0
    dir_stat: &Stat,
432
0
    name: &CStr,
433
0
    kind: Kind,
434
0
) -> io::Result<OwnedFd> {
435
0
    let (_, proc_stat) = proc()?;
436
437
    // Don't use `NOATIME`, because it [requires us to own the file], and when
438
    // a process sets itself non-dumpable Linux changes the user:group of its
439
    // `/proc/<pid>` files [to root:root].
440
    //
441
    // [requires us to own the file]: https://man7.org/linux/man-pages/man2/openat.2.html
442
    // [to root:root]: https://man7.org/linux/man-pages/man5/proc.5.html
443
0
    let mut oflags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NOCTTY;
444
0
    if let Kind::Symlink = kind {
445
0
        // Open symlinks with `O_PATH`.
446
0
        oflags |= OFlags::PATH;
447
0
    }
448
0
    let file = openat(dir, name, oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP)?;
449
0
    let file_stat = fstat(&file)?;
450
451
    // `is_mountpoint` only works on directory mount points, not file mount
452
    // points. To detect file mount points, scan the parent directory to see if
453
    // we can find a regular file with an inode and name that matches the file
454
    // we just opened. If we can't find it, there could be a file bind mount on
455
    // top of the file we want.
456
    //
457
    // TODO: With Linux 5.8 we might be able to use `statx` and
458
    // `STATX_ATTR_MOUNT_ROOT` to detect mountpoints directly instead of doing
459
    // this scanning.
460
461
0
    let expected_type = match kind {
462
0
        Kind::File => FileType::RegularFile,
463
0
        Kind::Symlink => FileType::Symlink,
464
0
        _ => unreachable!(),
465
    };
466
467
0
    let mut found_file = false;
468
0
    let mut found_dot = false;
469
0
470
0
    // Open a new fd, so that if we're called on multiple threads, they don't
471
0
    // share a seek position.
472
0
    let oflags =
473
0
        OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NOCTTY | OFlags::DIRECTORY;
474
0
    let dir = openat(dir, cstr!("."), oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP)?;
475
0
    let check_dir_stat = fstat(&dir)?;
476
0
    if check_dir_stat.st_dev != dir_stat.st_dev || check_dir_stat.st_ino != dir_stat.st_ino {
477
0
        return Err(io::Errno::NOTSUP);
478
0
    }
479
0
480
0
    // Position the directory iteration at the start.
481
0
    seek(&dir, SeekFrom::Start(0))?;
482
483
0
    let mut buf = [MaybeUninit::uninit(); 2048];
484
0
    let mut iter = RawDir::new(dir, &mut buf);
485
0
    while let Some(entry) = iter.next() {
486
0
        let entry = entry.map_err(|_err| io::Errno::NOTSUP)?;
487
0
        if entry.ino() == file_stat.st_ino
488
0
            && entry.file_type() == expected_type
489
0
            && entry.file_name() == name
490
        {
491
            // We found the file. Proceed to check the file handle.
492
0
            let _ = check_proc_entry_with_stat(kind, file.as_fd(), file_stat, Some(proc_stat))?;
493
494
0
            found_file = true;
495
0
        } else if entry.ino() == dir_stat.st_ino
496
0
            && entry.file_type() == FileType::Directory
497
0
            && entry.file_name() == cstr!(".")
498
0
        {
499
0
            // We found ".", and it's the right ".".
500
0
            found_dot = true;
501
0
        }
502
    }
503
504
0
    if found_file && found_dot {
505
0
        Ok(file)
506
    } else {
507
0
        Err(io::Errno::NOTSUP)
508
    }
509
0
}