From 265a940b63d22ee8ccde87bb8cfc340a4aa8387a Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 3 Sep 2026 14:59:41 +1000 Subject: [PATCH 1/5] gha: workaround for cargo-llvm-cov build-dir v2 regression Rust nightly-2026-07-30 enabled the v2 build-dir layout for builds, which /is/ supported by cargo-llvm-cov and cargo-nextest but unfortunately the way they handle it is not incompatible. cargo-llvm-cov relies on a special "fingerprint" directory being present to look for a v2 build-dir layout but "cargo-nextest archive" does not include that directory in the archive (instead the relevant information is stored in a metadata JSON file that cargo-llvm-cov should probably be parsing instead). Until the issue is fixed in cargo-llvm-cov (or cargo-nextest adds a workaround), work around the issue by creating the special "fingerprint" directory after we extract the cargo-nextest archive ourselves. Without this patch, the coverage-related CI jobs failed with the following error as cargo-llvm-cov couldn't find the right directory for the objects: error: failed to collect object files: not found object files (searched directories: .../target/llvm-cov-target/target/debug); this may occur if show-env subcommand is used incorrectly (see docs or other warnings), or unsupported commands or configs are used Signed-off-by: Aleksa Sarai --- .github/workflows/rust.yml | 22 ++++++++++++++++++++-- Dockerfile | 4 ++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7cce3f66..523c92f3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -382,9 +382,18 @@ jobs: # FIXME: llvm-cov appears to have some kind of bug with # --nextest-archive-file as they do not strip the "target" prefix from # the nextest archive. As a workaround, we just extract it ourselves. + # + # FIXME: Cargo's v2 build-dir layout (enabled since nightly-2026-07-30) + # causes issues because while both nextest and cargo-llvm-cov support it, + # cargo-llvm-cov depends on a magic "fingerprint" directory being present + # to detect v2 build-dir usage but nextest does not include in its + # archives. We can workaround it for now by creating the directory + # ourselves. See . - name: extract nextest archive - run: >- + run: |- tar xv -f nextest-pathrs-${{ matrix.run-as }}.tar.zst -C target/llvm-cov-target/ --strip-components=1 + # mkdir -p ./target/llvm-cov-target/debug/build///fingerprint + find target/llvm-cov-target/debug/build -mindepth 2 -maxdepth 2 -type d -exec mkdir -p '{}/fingerprint' ';' # Upload to CodeCov. - name: generate codecov-friendly coverage id: codecov-coverage @@ -562,9 +571,18 @@ jobs: # FIXME: llvm-cov appears to have some kind of bug with # --nextest-archive-file as they do not strip the "target" prefix from # the nextest archive. As a workaround, we just extract it ourselves. + # + # FIXME: Cargo's v2 build-dir layout (enabled since nightly-2026-07-30) + # causes issues because while both nextest and cargo-llvm-cov support it, + # cargo-llvm-cov depends on a magic "fingerprint" directory being present + # to detect v2 build-dir usage but nextest does not include in its + # archives. We can workaround it for now by creating the directory + # ourselves. See . - name: extract nextest archive - run: >- + run: |- tar xv -f nextest-pathrs-root.tar.zst -C target/llvm-cov-target/ --strip-components=1 + # mkdir -p ./target/llvm-cov-target/debug/build///fingerprint + find target/llvm-cov-target/debug/build -mindepth 2 -maxdepth 2 -type d -exec mkdir -p '{}/fingerprint' ';' - name: calculate coverage run: cargo llvm-cov report diff --git a/Dockerfile b/Dockerfile index 7655eee1..d7e6c234 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,9 +111,9 @@ RUN CARGO_BINSTALL_VERSION="$CARGO_BINSTALL_VERSION" \ curl -L --proto '=https' --tlsv1.2 -sSf \ "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/v$CARGO_BINSTALL_VERSION/install-from-binstall-release.sh" | bash -ARG CARGO_LLVM_COV_VERSION=0.8.7 +ARG CARGO_LLVM_COV_VERSION=0.9.0 ARG CARGO_HACK_VERSION=0.6.45 -ARG CARGO_NEXTEST_VERSION=0.9.137 +ARG CARGO_NEXTEST_VERSION=0.9.143 RUN cargo binstall --no-confirm \ "cargo-llvm-cov@$CARGO_LLVM_COV_VERSION" \ "cargo-hack@$CARGO_HACK_VERSION" \ From b11a652ef25e9fe43d9573153ced050131204bc8 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 3 Sep 2026 15:57:04 +1000 Subject: [PATCH 2/5] rust: use the toolchain's llvm-profdata When trying to figure out problems between cargo-nextest and cargo-llvm-cov, I ran into a few problems when using incompatible LLVM versions where the profdata format changed. We should prefer the toolchain's llvm-profdata over anything else. Signed-off-by: Aleksa Sarai --- Dockerfile | 4 ++-- hack/rust-tests.sh | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index d7e6c234..fccf1ce0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -121,8 +121,8 @@ RUN cargo binstall --no-confirm \ ARG RUST_NIGHTLY=nightly-2026-06-03 RUN rustup toolchain install "$RUST_NIGHTLY" && \ - rustup component add llvm-tools llvm-tools-preview && \ - rustup component add --toolchain "$RUST_NIGHTLY" llvm-tools llvm-tools-preview + rustup component add llvm-tools && \ + rustup component add --toolchain "$RUST_NIGHTLY" llvm-tools ENV CARGO_NIGHTLY="cargo +$RUST_NIGHTLY" # We want the installed libpathrs library for the Python and Go tests. diff --git a/hack/rust-tests.sh b/hack/rust-tests.sh index e8c512df..5c03cb50 100755 --- a/hack/rust-tests.sh +++ b/hack/rust-tests.sh @@ -158,10 +158,31 @@ fi bail "--enosys=$(strjoin , "${enosys_syscalls[@]}") contains invalid syscalls" } +# Find the llvm-profdata from the actual toolchain we are using. The raw +# profile format changes between LLVM versions and llvm-profdata requires an +# exact version match, so a distro-provided llvm-profdata could fail to merge +# profiles produced by Rust nightly. +function toolchain_llvm_profdata() { + local cargo_args + read -ra cargo_args <<<"$CARGO" + # The rustup wrappers are enabled for both cargo and rustc so if this is a + # "cargo" or "cargo +" command we can swap "cargo" for "rustc". + # Otherwise, fallback to the system toolchains. + [ "${cargo_args[0]}" == "cargo" ] || return 1 + + local libdir + libdir="$(rustc "${cargo_args[@]:1}" --print target-libdir 2>/dev/null)" || return 1 + local profdata + profdata="$(dirname "$libdir")/bin/llvm-profdata" + [ -x "$profdata" ] || return 1 + echo "$profdata" +} + function llvm-profdata() { local profdata - { command llvm-profdata --help &>/dev/null && profdata=llvm-profdata ; } || + { profdata="$(toolchain_llvm_profdata)" ; } || + { command llvm-profdata --help &>/dev/null && profdata=llvm-profdata ; } || { command rust-profdata --help &>/dev/null && profdata=rust-profdata ; } || { command cargo-profdata --help &>/dev/null && profdata=cargo-profdata ; } || bail "cannot find llvm-profdata!" From 5ab4fbc5ff1d8a65eb7c19e3831a2a835a4c6e72 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 2 Sep 2026 16:05:12 +1000 Subject: [PATCH 3/5] rustfmt: update comment wrapping Since nightly-2026-08-20 rustfmt has been indent-aware (which was implemented because of an issue we opened) and so we need to now re-format a lot of comments. Unfortunately, trailing comments on lines now get the same treatment quite aggressively so a lot of those needed to be moved to no longer be trailing comments (it would be great if this bit was configurable but no such luck). Signed-off-by: Aleksa Sarai --- .rustfmt.toml | 2 +- build.rs | 37 ++++++++++++------------ contrib/fake-enosys/src/main.rs | 5 ++-- src/capi/core.rs | 51 ++++++++++++++++++++++----------- src/capi/procfs.rs | 9 ++++-- src/capi/utils.rs | 8 +++--- src/resolvers/opath/imp.rs | 37 ++++++++++++------------ src/syscalls.rs | 4 +-- src/tests/traits/handle.rs | 3 +- src/utils/fd.rs | 12 ++++---- src/utils/fdinfo.rs | 15 +++++----- 11 files changed, 104 insertions(+), 79 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 8d5d1af8..3980286e 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -83,7 +83,7 @@ match_block_trailing_comma = false blank_lines_upper_bound = 1 blank_lines_lower_bound = 0 edition = "2021" -version = "One" +style_edition = "2021" inline_attribute_width = 0 merge_derives = true use_try_shorthand = false diff --git a/build.rs b/build.rs index 10e64367..3f0cee21 100644 --- a/build.rs +++ b/build.rs @@ -60,9 +60,9 @@ fn main() { }; if is_cdylib { let name = "pathrs"; - // TODO: Since we use symbol versioning, it seems quite unlikely that we - // would ever bump the major version in the SONAME, so we should - // probably hard-code this or define it elsewhere. + // TODO: Since we use symbol versioning, it seems quite unlikely + // that we would ever bump the major version in the SONAME, so we + // should probably hard-code this or define it elsewhere. let major = env::var("CARGO_PKG_VERSION_MAJOR").unwrap(); println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,lib{name}.so.{major}"); @@ -76,9 +76,9 @@ fn main() { .expect("mktemp should be utf-8 safe string"); writeln!( version_script_file, - // All of the symbol versions are done with in-line .symver entries. - // This version script is only needed to define the version nodes - // (and their dependencies). + // All of the symbol versions are done with in-line .symver + // entries. This version script is only needed to define the + // version nodes (and their dependencies). // FIXME: "local" doesn't appear to actually hide symbols in the // output .so. For more information about getting all of this to // work nicely, see . @@ -91,20 +91,21 @@ fn main() { .expect("write version script"); println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script={version_script_path}"); - // The above version script (and our .symver setup) conflicts with the - // version script and options used by Rust when linking with GNU ld. - // Thankfully, lld Just Works(TM) out of the box so we can use it. + // The above version script (and our .symver setup) conflicts with + // the version script and options used by Rust when linking with GNU + // ld. Thankfully, lld Just Works(TM) out of the box so we can use + // it. // - // Rust 1.90 switched to lld by default for x86, but for older versions - // and other architectures it is necessary to specify the linker as lld - // (there was also a rustflag for this but it was unstable until Rust - // 1.90). + // Rust 1.90 switched to lld by default for x86, but for older + // versions and other architectures it is necessary to specify the + // linker as lld (there was also a rustflag for this but it was + // unstable until Rust 1.90). // - // Unfortunately, while there are some clever tricks you could use for - // GNU ld (such as writing an ld wrapper and executing it with "cc -B"), - // doing so produces useless symbol versions so it's better to just - // require lld. Debian bullseye and later all have lld, so this is a - // non-issue for packagers. + // Unfortunately, while there are some clever tricks you could use + // for GNU ld (such as writing an ld wrapper and executing it with + // "cc -B"), doing so produces useless symbol versions so it's + // better to just require lld. Debian bullseye and later all have + // lld, so this is a non-issue for packagers. println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld"); } } diff --git a/contrib/fake-enosys/src/main.rs b/contrib/fake-enosys/src/main.rs index cb755125..a2a79728 100644 --- a/contrib/fake-enosys/src/main.rs +++ b/contrib/fake-enosys/src/main.rs @@ -104,8 +104,9 @@ fn main() -> Result<(), Error> { if !syscalls.is_empty() { let mut filter = bpf::compile_filter(&syscalls)?; - // Unprivileged processes cannot enable seccomp-bpf unless they also set the - // no-new-privs bit (to stop them from being able to trick setuid binaries). + // Unprivileged processes cannot enable seccomp-bpf unless they also set + // the no-new-privs bit (to stop them from being able to trick setuid + // binaries). if !rustix_process::getuid().is_root() { rustix_thread::set_no_new_privs(true).context("could not set no-new-privs bit")?; } diff --git a/src/capi/core.rs b/src/capi/core.rs index 761d9db3..c573dc2d 100644 --- a/src/capi/core.rs +++ b/src/capi/core.rs @@ -75,7 +75,8 @@ use libc::{c_char, c_int, c_uint, dev_t, size_t}; /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_open_root(path: *const c_char) -> RawFd { - unsafe { utils::parse_path(path) } // SAFETY: C caller says path is safe. + // SAFETY: C caller guarantees path is safe. + unsafe { utils::parse_path(path) } .and_then(Root::open) .into_c_return() } @@ -160,7 +161,8 @@ pub unsafe extern "C" fn pathrs_inroot_resolve( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; root.resolve(path) }() .into_c_return() @@ -196,7 +198,8 @@ pub unsafe extern "C" fn pathrs_inroot_resolve_nofollow( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; root.resolve_nofollow(path) }() .into_c_return() @@ -241,7 +244,8 @@ pub unsafe extern "C" fn pathrs_inroot_open( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; let flags = OpenFlags::from_bits_retain(flags); root.open_subpath(path, flags) }() @@ -315,7 +319,8 @@ pub unsafe extern "C" fn pathrs_inroot_readlink( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; let link_target = root.readlink(path)?; // SAFETY: C caller guarantees buffer is at least linkbuf_size and can // be written to. @@ -369,8 +374,10 @@ pub unsafe extern "C" fn pathrs_inroot_rename( })?; } let root = RootRef::from_fd(new_root_fd); - let old_path = unsafe { utils::parse_path(old_path) }?; // SAFETY: C caller guarantees path is safe. - let new_path = unsafe { utils::parse_path(new_path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let old_path = unsafe { utils::parse_path(old_path) }?; + // SAFETY: C caller guarantees path is safe. + let new_path = unsafe { utils::parse_path(new_path) }?; root.rename(old_path, new_path, rflags) }() @@ -426,7 +433,8 @@ pub unsafe extern "C" fn pathrs_inroot_rmdir( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; root.remove_dir(path) }() .into_c_return() @@ -461,7 +469,8 @@ pub unsafe extern "C" fn pathrs_inroot_unlink( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; root.remove_file(path) }() .into_c_return() @@ -493,7 +502,8 @@ pub unsafe extern "C" fn pathrs_inroot_remove_all( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; root.remove_all(path) }() .into_c_return() @@ -551,7 +561,8 @@ pub unsafe extern "C" fn pathrs_inroot_creat( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; let mode = mode & !libc::S_IFMT; let perm = Permissions::from_mode(mode); root.create_file(path, OpenFlags::from_bits_retain(flags), &perm) @@ -636,7 +647,8 @@ pub unsafe extern "C" fn pathrs_inroot_mkdir_all( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; let perm = Permissions::from_mode(mode); root.mkdir_all(path, &perm) }() @@ -671,7 +683,8 @@ pub unsafe extern "C" fn pathrs_inroot_mknod( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let path = unsafe { utils::parse_path(path)? }; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path)? }; let fmt = mode & libc::S_IFMT; let perms = Permissions::from_mode(mode ^ fmt); @@ -721,8 +734,10 @@ pub unsafe extern "C" fn pathrs_inroot_symlink( || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); - let target = unsafe { utils::parse_path(target)? }; // SAFETY: C caller guarantees path is safe. - let linkpath = unsafe { utils::parse_path(linkpath)? }; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let target = unsafe { utils::parse_path(target)? }; + // SAFETY: C caller guarantees path is safe. + let linkpath = unsafe { utils::parse_path(linkpath)? }; root.create(linkpath, &InodeType::Symlink(target.into())) }() .into_c_return() @@ -798,8 +813,10 @@ pub unsafe extern "C" fn pathrs_inroot_hardlink( })?; } let root = RootRef::from_fd(new_root_fd); - let old_path = unsafe { utils::parse_path(old_path) }?; // SAFETY: C caller guarantees path is safe. - let new_path = unsafe { utils::parse_path(new_path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let old_path = unsafe { utils::parse_path(old_path) }?; + // SAFETY: C caller guarantees path is safe. + let new_path = unsafe { utils::parse_path(new_path) }?; root.create(new_path, &InodeType::Hardlink(old_path.into())) }() .into_c_return() diff --git a/src/capi/procfs.rs b/src/capi/procfs.rs index e73e91b5..d6b2d448 100644 --- a/src/capi/procfs.rs +++ b/src/capi/procfs.rs @@ -222,7 +222,8 @@ impl From for CProcfsBase { // type_of::MAX & _PATHRS_PROC_TYPE_MASK, // 0, // ); - // static_assertions::const_assert_eq!(type_of::MAX, u32::MAX); + // static_assertions::const_assert_eq!(type_of::MAX, + // u32::MAX); // We know this to be true from the check in the above TryFrom // impl for ProcfsBase, but add an assertion here since we @@ -468,7 +469,8 @@ pub unsafe extern "C" fn pathrs_proc_openat( ) -> RawFd { || -> Result<_, Error> { let base = base.try_into()?; - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; let oflags = OpenFlags::from_bits_retain(flags); let procfs = parse_proc_rootfd(proc_rootfd)?; @@ -605,7 +607,8 @@ pub unsafe extern "C" fn pathrs_proc_readlinkat( ) -> c_int { || -> Result<_, Error> { let base = base.try_into()?; - let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. + // SAFETY: C caller guarantees path is safe. + let path = unsafe { utils::parse_path(path) }?; let procfs = parse_proc_rootfd(proc_rootfd)?; let link_target = procfs.readlink(base, path)?; // SAFETY: C caller guarantees buffer is at least linkbuf_size and can diff --git a/src/capi/utils.rs b/src/capi/utils.rs index 321a43cc..03c1fc40 100644 --- a/src/capi/utils.rs +++ b/src/capi/utils.rs @@ -182,10 +182,10 @@ impl<'fd> CBorrowedFd<'fd> { } .into()) } else { - // SAFETY: The C caller guarantees that the file descriptor is valid for - // the lifetime of CBorrowedFd (which is the same lifetime as - // BorrowedFd). We verify that the file descriptor is not - // negative, so it is definitely valid. + // SAFETY: The C caller guarantees that the file descriptor is valid + // for the lifetime of CBorrowedFd (which is the same lifetime as + // BorrowedFd). We verify that the file descriptor is not negative, + // so it is definitely valid. Ok(unsafe { BorrowedFd::borrow_raw(self.inner) }) } } diff --git a/src/resolvers/opath/imp.rs b/src/resolvers/opath/imp.rs index 047e8c6c..b6f2f25f 100644 --- a/src/resolvers/opath/imp.rs +++ b/src/resolvers/opath/imp.rs @@ -381,8 +381,9 @@ fn do_resolve( current = next.into(); continue; } else { - // If we hit the last component and we were told to not follow - // the trailing symlink, just return the link we have. + // If we hit the last component and we were told to not + // follow the trailing symlink, just return the link we + // have. if remaining_components.is_empty() && no_follow_trailing { current = next.into(); break; @@ -439,24 +440,24 @@ fn do_resolve( source: err, })?; - // Check if it's a good idea to walk this symlink. If we are on - // a filesystem that supports magic-links and we've hit an - // absolute symlink, it is incredibly likely that this component - // is a magic-link and it makes no sense to try to resolve it in - // userspace. + // Check if it's a good idea to walk this symlink. If we are + // on a filesystem that supports magic-links and we've hit + // an absolute symlink, it is incredibly likely that this + // component is a magic-link and it makes no sense to try to + // resolve it in userspace. // // NOTE: There are some pseudo-magic-links like /proc/self - // (which dynamically generates the symlink contents but doesn't - // use nd_jump_link). In the case of procfs, these are always - // relative, and they are reasonable for us to walk. + // (which dynamically generates the symlink contents but + // doesn't use nd_jump_link). In the case of procfs, these + // are always relative, and they are reasonable for us to + // walk. In procfs, all real magic-links use d_path() to + // generate readlink() and thus are all absolute paths. // - // In procfs, all magic-links use d_path() to generate - // readlink() and thus are all absolute paths. (Unfortunately, - // apparmorfs uses nd_jump_link to make + // (Unfortunately, apparmorfs uses nd_jump_link to make // /sys/kernel/security/apparmor/policy dynamic using actual - // nd_jump_link() and their readlink give us a dummy relative - // path like "apparmorfs:[123]". But in that case we will just - // get an error.) + // nd_jump_link() and their readlink give us a dummy + // relative path like "apparmorfs:[123]". But in that case + // we will just get an error.) if link_target.is_absolute() && next .is_magiclink_filesystem() @@ -483,8 +484,8 @@ fn do_resolve( // Remove the link component from our expectex path. expected_path.pop(); - // Add contents of the symlink to the set of components we are - // looping over. + // Add contents of the symlink to the set of components we + // are looping over. link_target .raw_components() .prepend(&mut remaining_components); diff --git a/src/syscalls.rs b/src/syscalls.rs index 3addf9ce..ce14b4fa 100644 --- a/src/syscalls.rs +++ b/src/syscalls.rs @@ -816,8 +816,8 @@ pub(crate) mod openat2 { } /// Wrapper for `openat2(2)` which auto-sets `O_CLOEXEC | O_NOCTTY`. - // NOTE: rustix's openat2 wrapper is not extensible-friendly so we use our own - // for now. See . + // NOTE: rustix's openat2 wrapper is not extensible-friendly so we use our + // own for now. See . pub(crate) fn openat2_follow( dirfd: impl AsFd, path: impl AsRef, diff --git a/src/tests/traits/handle.rs b/src/tests/traits/handle.rs index c2de8e25..2ec85f6f 100644 --- a/src/tests/traits/handle.rs +++ b/src/tests/traits/handle.rs @@ -41,8 +41,9 @@ pub(in crate::tests) trait HandleImpl: AsFd + std::fmt::Debug + Sized { type Cloned: HandleImpl + Into; type Error: ErrorImpl; + // Only used by the capi tests. + #[cfg_attr(not(feature = "capi"), allow(dead_code))] // NOTE: We return Self::Cloned so that we can share types with HandleRef. - #[cfg_attr(not(feature = "capi"), allow(dead_code))] // this method is only used by capi tests fn from_fd(fd: impl Into) -> Self::Cloned; fn try_clone(&self) -> Result; diff --git a/src/utils/fd.rs b/src/utils/fd.rs index 2891f29e..0a6b56bb 100644 --- a/src/utils/fd.rs +++ b/src/utils/fd.rs @@ -434,13 +434,13 @@ pub(crate) fn fetch_mnt_id( // fails. This does require us to operate on procfs in a less-safe way // (unlike the alternative approaches), however note that: // - // * For openat2(2) systems, this is completely safe (fdinfo files are regular - // files, and thus -- unlike magic-links -- RESOLVE_NO_XDEV can be used to - // safely protect against bind-mounts). + // * For openat2(2) systems, this is completely safe (fdinfo files are + // regular files, and thus -- unlike magic-links -- RESOLVE_NO_XDEV can + // be used to safely protect against bind-mounts). // - // * For non-openat2(2) systems, an attacker can theoretically attack this by - // overmounting fdinfo with something like /proc/self/environ and fill it - // with a fake fdinfo file. + // * For non-openat2(2) systems, an attacker can theoretically attack this + // by overmounting fdinfo with something like /proc/self/environ and fill + // it with a fake fdinfo file. // // However, get_fdinfo_field and fd_get_verify_fdinfo have enough extra // protections that would probably make it infeasible for an attacker to diff --git a/src/utils/fdinfo.rs b/src/utils/fdinfo.rs index cb4275b0..28423b47 100644 --- a/src/utils/fdinfo.rs +++ b/src/utils/fdinfo.rs @@ -104,14 +104,15 @@ where // our file descriptor. This makes attacks harder (if not near impossible, // outside of very constrained situations): // - // * An attacker would probably struggle to always accurately guess the inode - // number of files that the process is trying to operate on. Yes, if they know - // the victim process's access patterns of procfs they could probably make an - // educated guess, but most files do not have stable inode numbers in procfs. + // * An attacker would probably struggle to always accurately guess the + // inode number of files that the process is trying to operate on. Yes, if + // they know the victim process's access patterns of procfs they could + // probably make an educated guess, but most files do not have stable + // inode numbers in procfs. // - // * An attacker can no longer bind-mount their own fdinfo directory with just a - // buch of handles to "/proc" open (assuming the attacker is trying to spoof - // "mnt_id"), because the inode numbers won't match. + // * An attacker can no longer bind-mount their own fdinfo directory with + // just a buch of handles to "/proc" open (assuming the attacker is trying + // to spoof "mnt_id"), because the inode numbers won't match. // // They also can't really fake inode numbers in real procfs fdinfo files, // so they would need to create fake fdinfo files using individual file From 966df44b6a45fbd5e2ac2ff75f059460cf392897 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Fri, 28 Aug 2026 16:37:14 +1000 Subject: [PATCH 4/5] python: bump minimum to 3.10 and fix lints Ruff 0.16 will auto-fix imports in a way that is incompatible with pre-3.10 Python versions so it might be time to upgrade. Signed-off-by: Aleksa Sarai --- .github/workflows/bindings-python.yml | 4 +- .github/workflows/e2e-tests.yml | 2 +- contrib/bindings/python/README.md | 6 +- contrib/bindings/python/pathrs/__init__.py | 3 +- contrib/bindings/python/pathrs/_internal.py | 52 ++++++++--------- .../python/pathrs/_libpathrs_cffi/lib.pyi | 57 ++++++++----------- contrib/bindings/python/pathrs/_pathrs.py | 27 ++++----- .../bindings/python/pathrs/pathrs_build.py | 12 ++-- contrib/bindings/python/pathrs/procfs.py | 25 ++++---- contrib/bindings/python/pyproject.toml | 2 +- contrib/bindings/python/setup.py | 6 +- e2e-tests/cmd/python/pathrs-cmd.py | 23 ++++---- examples/python/cat.py | 9 ++- examples/python/static_web.py | 15 +++-- 14 files changed, 117 insertions(+), 126 deletions(-) diff --git a/.github/workflows/bindings-python.yml b/.github/workflows/bindings-python.yml index 1ea311e9..09065dc4 100644 --- a/.github/workflows/bindings-python.yml +++ b/.github/workflows/bindings-python.yml @@ -60,7 +60,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.x"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.x"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -105,7 +105,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.x"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.x"] needs: - build-pyproject runs-on: ubuntu-latest diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3617fe52..7f6484af 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -41,7 +41,7 @@ jobs: lang-desc: [""] include: # Test minimum python version. - # TODO: Switch to python 3.9 (pathrs bindings version). + # TODO: Switch to python 3.10 (pathrs bindings version). # typing.Self: python >= 3.11 # match: python >= 3.10 - lang: python diff --git a/contrib/bindings/python/README.md b/contrib/bindings/python/README.md index 354b0104..c4e424df 100644 --- a/contrib/bindings/python/README.md +++ b/contrib/bindings/python/README.md @@ -38,11 +38,11 @@ RENAME_EXCHANGE = 0x2 with pathrs.Root("/path/to/rootfs") as root: # symlink - root.symlink("foo", "bar") # foo -> bar + root.symlink("foo", "bar") # foo -> bar # link - root.hardlink("a", "b") # a -> b + root.hardlink("a", "b") # a -> b # rename(at2) - root.rename("foo", "b", flags=RENAME_EXCHANGE) # foo <-> b + root.rename("foo", "b", flags=RENAME_EXCHANGE) # foo <-> b # open(O_CREAT) with root.creat("newfile", "w+") as f: f.write("Some contents.") diff --git a/contrib/bindings/python/pathrs/__init__.py b/contrib/bindings/python/pathrs/__init__.py index d9b3438a..6454c7ec 100644 --- a/contrib/bindings/python/pathrs/__init__.py +++ b/contrib/bindings/python/pathrs/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux @@ -13,7 +12,7 @@ import importlib.metadata from . import _pathrs -from ._pathrs import * # noqa: F403 # We just re-export everything. +from ._pathrs import * # We just re-export everything. # In order get pydoc to include the documentation for the re-exported code from # _pathrs, we need to include all of the members in __all__. Rather than diff --git a/contrib/bindings/python/pathrs/_internal.py b/contrib/bindings/python/pathrs/_internal.py index cd7d3eef..776fde6e 100644 --- a/contrib/bindings/python/pathrs/_internal.py +++ b/contrib/bindings/python/pathrs/_internal.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux @@ -9,19 +8,18 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -import io -import os -import sys import copy import errno import fcntl - +import io +import os +import sys import typing from types import TracebackType -from typing import Any, Dict, IO, Optional, TextIO, Type, TypeVar, Union +from typing import IO, Any, ClassVar, TextIO, TypeAlias, TypeVar # TODO: Remove this once we only support Python >= 3.11. -from typing_extensions import Self, TypeAlias +from typing_extensions import Self from ._libpathrs_cffi import lib as libpathrs_so @@ -52,7 +50,7 @@ def _pystr(cstr: CString) -> str: def _cbuffer(size: int) -> CBuffer: - return ffi.new("char[%d]" % (size,)) + return ffi.new(f"char[{size}]") def _is_pathrs_err(ret: int) -> bool: @@ -68,10 +66,10 @@ class PathrsError(Exception): """ message: str - errno: Optional[int] - strerror: Optional[str] + errno: int | None + strerror: str | None - def __init__(self, message: str, /, *, errno: Optional[int] = None): + def __init__(self, message: str, /, *, errno: int | None = None): # Construct Exception. super().__init__(message) @@ -88,7 +86,7 @@ def __init__(self, message: str, /, *, errno: Optional[int] = None): self.strerror = str(errno) @classmethod - def _fetch(cls, err_id: int, /) -> Optional[Self]: + def _fetch(cls, err_id: int, /) -> Self | None: if err_id >= 0: return None @@ -109,10 +107,10 @@ def __str__(self) -> str: if self.errno is None: return self.message else: - return "%s (%s)" % (self.message, self.strerror) + return f"{self.message} ({self.strerror})" def __repr__(self) -> str: - return "Error(%r, errno=%r)" % (self.message, self.errno) + return f"Error({self.message!r}, errno={self.errno!r})" def pprint(self, out: TextIO = sys.stdout) -> None: "Pretty-print the error to the given @out file." @@ -120,8 +118,8 @@ def pprint(self, out: TextIO = sys.stdout) -> None: if self.errno is None: print("pathrs error:", file=out) else: - print("pathrs error [%s]:" % (self.strerror,), file=out) - print(" %s" % (self.message,), file=out) + print(f"pathrs error [{self.strerror}]:", file=out) + print(f" {self.message}", file=out) INTERNAL_ERROR = PathrsError("tried to fetch libpathrs error but no error found") @@ -131,7 +129,7 @@ class FilenoFile(typing.Protocol): def fileno(self) -> int: ... -FileLike = Union[FilenoFile, int] +FileLike = FilenoFile | int def _fileno(file: FileLike) -> int: @@ -151,7 +149,7 @@ def _clonefile(file: FileLike) -> int: Fd = TypeVar("Fd", bound="WrappedFd") -class WrappedFd(object): +class WrappedFd: """ Represents a file descriptor that allows for manual lifetime management, unlike os.fdopen() which are tracked by the GC with no way of "leaking" the @@ -160,7 +158,7 @@ class WrappedFd(object): pathrs will return WrappedFds for most operations that return an fd. """ - _fd: Optional[int] + _fd: int | None def __init__(self, file: FileLike, /): """ @@ -233,12 +231,12 @@ def fdopen(self, mode: str = "r") -> IO[Any]: raise @classmethod - def from_raw_fd(cls: Type[Fd], fd: int, /) -> Fd: + def from_raw_fd(cls, fd: int, /) -> Self: "Shorthand for WrappedFd(fd)." return cls(fd) @classmethod - def from_file(cls: Type[Fd], file: FileLike, /) -> Fd: + def from_file(cls, file: FileLike, /) -> Self: "Shorthand for WrappedFd(file)." return cls(file) @@ -288,7 +286,7 @@ def __copy__(self) -> Self: # A "shallow copy" of a file is the same as a deep copy. return copy.deepcopy(self) - def __deepcopy__(self, memo: Dict[int, Any]) -> Self: + def __deepcopy__(self, memo: dict[int, Any]) -> Self: "Identical to WrappedFd.clone()" return self.clone() @@ -301,9 +299,9 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - exc_traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: TracebackType | None, ) -> None: self.close() @@ -349,9 +347,9 @@ def _convert_mode(mode: str) -> int: class SingletonClass(type): """Metaclass used to create singleton classes.""" - _instances: dict[type, Type[Any]] = {} + _instances: ClassVar[dict[type, type[Any]]] = {} def __call__(cls, *args, **kwargs): # type: ignore[no-untyped-def] # TODO: Not clear what annotations to use, and mypy appears to be confused by metaclasses. if cls not in cls._instances: - cls._instances[cls] = super(SingletonClass, cls).__call__(*args, **kwargs) + cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls] diff --git a/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi b/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi index 92b3c3da..88017e64 100644 --- a/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi +++ b/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi @@ -8,10 +8,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import type_check_only, Union - -# TODO: Remove this once we only support Python >= 3.10. -from typing_extensions import TypeAlias, Literal +from typing import Literal, TypeAlias, type_check_only from .._pathrs import CBuffer, CString from ..procfs import ProcfsBase @@ -29,7 +26,7 @@ __PATHRS_MAX_ERR_VALUE: ErrorId # TODO: We actually return Union[CError, cffi.FFI.NULL] but we can't express # this using the typing stubs for CFFI... -def pathrs_errorinfo(err_id: Union[ErrorId, int]) -> CError: ... +def pathrs_errorinfo(err_id: ErrorId | int) -> CError: ... def pathrs_errorinfo_free(err: CError) -> None: ... # pathrs_version_info_t * @@ -37,7 +34,7 @@ def pathrs_errorinfo_free(err: CError) -> None: ... class VersionInfo: version_string: CString -def pathrs_version(info: VersionInfo, size: int) -> Union[ErrorId, int]: ... +def pathrs_version(info: VersionInfo, size: int) -> ErrorId | int: ... # uint64_t ProcfsOpenFlags: TypeAlias = int @@ -58,70 +55,64 @@ __PATHRS_PROC_TYPE_PID: ProcfsBase PATHRS_PROC_DEFAULT_ROOTFD: RawFd # procfs API -def pathrs_procfs_open(how: ProcfsOpenHow, size: int) -> Union[RawFd, ErrorId]: ... +def pathrs_procfs_open(how: ProcfsOpenHow, size: int) -> RawFd | ErrorId: ... def pathrs_proc_open( base: ProcfsBase, path: CString, flags: int -) -> Union[RawFd, ErrorId]: ... +) -> RawFd | ErrorId: ... def pathrs_proc_openat( proc_root_fd: RawFd, base: ProcfsBase, path: CString, flags: int -) -> Union[RawFd, ErrorId]: ... +) -> RawFd | ErrorId: ... def pathrs_proc_readlink( base: ProcfsBase, path: CString, linkbuf: CBuffer, linkbuf_size: int -) -> Union[int, ErrorId]: ... +) -> int | ErrorId: ... def pathrs_proc_readlinkat( proc_root_fd: RawFd, base: ProcfsBase, path: CString, linkbuf: CBuffer, linkbuf_size: int, -) -> Union[int, ErrorId]: ... +) -> int | ErrorId: ... # core API -def pathrs_open_root(path: CString) -> Union[RawFd, ErrorId]: ... -def pathrs_reopen(fd: RawFd, flags: int) -> Union[RawFd, ErrorId]: ... -def pathrs_inroot_resolve(rootfd: RawFd, path: CString) -> Union[RawFd, ErrorId]: ... -def pathrs_inroot_resolve_nofollow( - rootfd: RawFd, path: CString -) -> Union[RawFd, ErrorId]: ... -def pathrs_inroot_open( - rootfd: RawFd, path: CString, flags: int -) -> Union[RawFd, ErrorId]: ... +def pathrs_open_root(path: CString) -> RawFd | ErrorId: ... +def pathrs_reopen(fd: RawFd, flags: int) -> RawFd | ErrorId: ... +def pathrs_inroot_resolve(rootfd: RawFd, path: CString) -> RawFd | ErrorId: ... +def pathrs_inroot_resolve_nofollow(rootfd: RawFd, path: CString) -> RawFd | ErrorId: ... +def pathrs_inroot_open(rootfd: RawFd, path: CString, flags: int) -> RawFd | ErrorId: ... def pathrs_inroot_creat( rootfd: RawFd, path: CString, flags: int, filemode: int -) -> Union[RawFd, ErrorId]: ... +) -> RawFd | ErrorId: ... def pathrs_inroot_rename( old_rootfd: RawFd, old_path: CString, new_rootfd: RawFd, new_path: CString, flags: int, -) -> Union[Literal[0], ErrorId]: ... -def pathrs_inroot_rmdir(rootfd: RawFd, path: CString) -> Union[Literal[0], ErrorId]: ... -def pathrs_inroot_unlink( - rootfd: RawFd, path: CString -) -> Union[Literal[0], ErrorId]: ... -def pathrs_inroot_remove_all(rootfd: RawFd, path: CString) -> Union[RawFd, ErrorId]: ... +) -> Literal[0] | ErrorId: ... +def pathrs_inroot_rmdir(rootfd: RawFd, path: CString) -> Literal[0] | ErrorId: ... +def pathrs_inroot_unlink(rootfd: RawFd, path: CString) -> Literal[0] | ErrorId: ... +def pathrs_inroot_remove_all(rootfd: RawFd, path: CString) -> RawFd | ErrorId: ... def pathrs_inroot_mkdir( rootfd: RawFd, path: CString, mode: int -) -> Union[Literal[0], ErrorId]: ... +) -> Literal[0] | ErrorId: ... def pathrs_inroot_mkdir_all( rootfd: RawFd, path: CString, mode: int -) -> Union[Literal[0], ErrorId]: ... +) -> Literal[0] | ErrorId: ... def pathrs_inroot_mknod( rootfd: RawFd, path: CString, mode: int, dev: int -) -> Union[Literal[0], ErrorId]: ... +) -> Literal[0] | ErrorId: ... def pathrs_inroot_hardlink( old_rootfd: RawFd, old_path: CString, new_rootfd: RawFd, new_path: CString, flags: int, -) -> Union[Literal[0], ErrorId]: ... +) -> Literal[0] | ErrorId: ... def pathrs_inroot_symlink( target: CString, rootfd: RawFd, linkpath: CString, -) -> Union[Literal[0], ErrorId]: ... +) -> Literal[0] | ErrorId: ... def pathrs_inroot_readlink( rootfd: RawFd, path: CString, linkbuf: CBuffer, linkbuf_size: int -) -> Union[int, ErrorId]: ... +) -> int | ErrorId: ... diff --git a/contrib/bindings/python/pathrs/_pathrs.py b/contrib/bindings/python/pathrs/_pathrs.py index 9d691f4c..120c2c5d 100644 --- a/contrib/bindings/python/pathrs/_pathrs.py +++ b/contrib/bindings/python/pathrs/_pathrs.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux @@ -10,29 +9,27 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import os - import typing -from typing import Any, IO, Union, cast import warnings # TODO: Remove this once we only support Python >= 3.11. -from typing_extensions import TypeAlias +from typing import IO, Any, TypeAlias, cast from ._internal import ( - # Generic helpers. - SingletonClass, + INTERNAL_ERROR, # File type helpers. FileLike, - WrappedFd, - _convert_mode, # Error API. PathrsError, - _is_pathrs_err, - INTERNAL_ERROR, + # Generic helpers. + SingletonClass, + WrappedFd, + _cbuffer, + _convert_mode, # CFFI helpers. _cstr, + _is_pathrs_err, _pystr, - _cbuffer, ) from ._libpathrs_cffi import lib as libpathrs_so @@ -52,12 +49,12 @@ CBuffer: TypeAlias = ffi.CData __all__ = [ - # Core api. - "Root", "Handle", - "library_version", # Error api (re-export). "PathrsError", + # Core api. + "Root", + "library_version", ] @@ -133,7 +130,7 @@ class Root(WrappedFd): relative to. """ - def __init__(self, file_or_path: Union[FileLike, str], /): + def __init__(self, file_or_path: FileLike | str, /): """ Create a handle from a file-like object or a path to a directory. diff --git a/contrib/bindings/python/pathrs/pathrs_build.py b/contrib/bindings/python/pathrs/pathrs_build.py index e378ed40..0e461b1c 100755 --- a/contrib/bindings/python/pathrs/pathrs_build.py +++ b/contrib/bindings/python/pathrs/pathrs_build.py @@ -13,12 +13,14 @@ # build of libpathrs, and can be redistributed alongside the pathrs.py wrapping # library). It's much better than the ABI-mode of CFFI. -import re +# TODO: Remove this once we only support Python >= 3.10. +from __future__ import annotations # PEP 604 + import os +import re import sys - -from typing import Any, Optional from collections.abc import Iterable +from typing import Any import cffi @@ -97,7 +99,7 @@ def find_rootdir() -> str: return root_dir -def srcdir_ffibuilder(root_dir: Optional[str] = None) -> cffi.FFI: +def srcdir_ffibuilder(root_dir: str | None = None) -> cffi.FFI: """ Build the CFFI bindings using the provided root_dir as the root of a pathrs source tree which has compiled cdylibs ready in target/*. @@ -108,7 +110,7 @@ def srcdir_ffibuilder(root_dir: Optional[str] = None) -> cffi.FFI: # Figure out which libs are usable. library_dirs: Iterable[str] = ( - os.path.join(root_dir, "target/%s/libpathrs.so" % (mode,)) + os.path.join(root_dir, f"target/{mode}/libpathrs.so") for mode in ("debug", "release") ) library_dirs = (so_path for so_path in library_dirs if os.path.exists(so_path)) diff --git a/contrib/bindings/python/pathrs/procfs.py b/contrib/bindings/python/pathrs/procfs.py index 088dd4a8..b7b6f01f 100644 --- a/contrib/bindings/python/pathrs/procfs.py +++ b/contrib/bindings/python/pathrs/procfs.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux @@ -10,22 +9,22 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import typing -from typing import Any, IO, cast +from typing import IO, Any, TypeAlias, cast # TODO: Remove this once we only support Python >= 3.11. -from typing_extensions import Self, TypeAlias +from typing_extensions import Self from ._internal import ( + INTERNAL_ERROR, + # Error API. + PathrsError, # File type helpers. WrappedFd, + _cbuffer, _convert_mode, - # Error API. - PathrsError, - _is_pathrs_err, - INTERNAL_ERROR, # CFFI helpers. _cstr, - _cbuffer, + _is_pathrs_err, ) from ._libpathrs_cffi import lib as libpathrs_so @@ -45,10 +44,10 @@ CBuffer: TypeAlias = ffi.CData __all__ = [ + "PROC_PID", "PROC_ROOT", "PROC_SELF", "PROC_THREAD_SELF", - "PROC_PID", "ProcfsHandle", # Shorthand for ProcfsHandle.cached().. "open", @@ -94,7 +93,13 @@ def PROC_PID(pid: int) -> ProcfsBase: class ProcfsHandle(WrappedFd): - """ """ + """ + A handle to a procfs root that can be operated on safely. + + While you can create your own custom handles with ProcfsHandle.new(), most + users should use the module-level procfs.* helper functions, which are all + shorthand for ProcfsHandle.cached().*. + """ _PROCFS_OPEN_HOW_TYPE = "pathrs_procfs_open_how *" diff --git a/contrib/bindings/python/pyproject.toml b/contrib/bindings/python/pyproject.toml index aee9ff82..8f3ba47d 100644 --- a/contrib/bindings/python/pyproject.toml +++ b/contrib/bindings/python/pyproject.toml @@ -44,7 +44,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] -requires-python = ">= 3.9" +requires-python = ">= 3.10" dependencies = [ "cffi>=1.10.0", "typing_extensions>=4.0.0", # TODO: Remove this once we only support Python >= 3.11. diff --git a/contrib/bindings/python/setup.py b/contrib/bindings/python/setup.py index 684799b9..ff65df41 100755 --- a/contrib/bindings/python/setup.py +++ b/contrib/bindings/python/setup.py @@ -9,13 +9,13 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -import setuptools +from typing import Any -from typing import Any, Dict +import setuptools # This is only needed for backwards compatibility with older versions. -def parse_pyproject() -> Dict[str, Any]: +def parse_pyproject() -> dict[str, Any]: try: import tomllib diff --git a/e2e-tests/cmd/python/pathrs-cmd.py b/e2e-tests/cmd/python/pathrs-cmd.py index 4139aa0d..ec001b2b 100755 --- a/e2e-tests/cmd/python/pathrs-cmd.py +++ b/e2e-tests/cmd/python/pathrs-cmd.py @@ -9,16 +9,17 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +import argparse import os -import sys import stat -import argparse -from typing import Optional, Self, Sequence, Protocol, Tuple +import sys +from collections.abc import Sequence +from typing import Protocol, Self sys.path.append(os.path.dirname(__file__) + "/../../../contrib/bindings/python") import pathrs from pathrs import procfs -from pathrs.procfs import ProcfsHandle, ProcfsBase +from pathrs.procfs import ProcfsBase, ProcfsHandle def version(args: argparse.Namespace): @@ -38,7 +39,7 @@ def root_resolve(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath follow: bool = args.follow - reopen: Optional[int] = args.reopen + reopen: int | None = args.reopen with root.resolve(subpath, follow_trailing=follow) as handle: print("HANDLE-PATH", fdpath(handle)) @@ -197,7 +198,7 @@ def procfs_readlink(args: argparse.Namespace): def parse_args( args: tuple[str, ...], -) -> Tuple[argparse.ArgumentParser, argparse.Namespace]: +) -> tuple[argparse.ArgumentParser, argparse.Namespace]: parser = argparse.ArgumentParser(prog="pathrs-cmd") parser.set_defaults(func=None) top_subparser = parser.add_subparsers() @@ -207,7 +208,7 @@ def add_mode_flag( name: str, default: int = 0o644, required: bool = False, - help: Optional[str] = None, + help: str | None = None, ) -> None: parser.add_argument( f"--{name}", @@ -264,9 +265,9 @@ def parse_oflags(flags: str) -> int: def add_o_flag( parser: argparse.ArgumentParser, name: str, - default: Optional[int] = os.O_RDONLY, + default: int | None = os.O_RDONLY, required: bool = False, - help: Optional[str] = None, + help: str | None = None, ) -> None: parser.add_argument( f"--{name}", @@ -378,8 +379,8 @@ def __call__( self: Self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Optional[str | Sequence[str]], - option_string: Optional[str] = None, + values: str | Sequence[str] | None, + option_string: str | None = None, ): inode_type: str dev: int diff --git a/examples/python/cat.py b/examples/python/cat.py index 609fdd05..c842e516 100755 --- a/examples/python/cat.py +++ b/examples/python/cat.py @@ -30,11 +30,10 @@ def chomp(s): def main(root_path, unsafe_path): # Test that context managers work properly with WrappedFd: - with pathrs.Root(root_path) as root: - with root.open(unsafe_path, "r") as f: - for line in f: - line = chomp(line) - print(line) + with pathrs.Root(root_path) as root, root.open(unsafe_path, "r") as f: + for line in f: + line = chomp(line) + print(line) if __name__ == "__main__": diff --git a/examples/python/static_web.py b/examples/python/static_web.py index 726f14c6..9263276a 100755 --- a/examples/python/static_web.py +++ b/examples/python/static_web.py @@ -14,10 +14,10 @@ # An example program which provides a static webserver which will serve files # from a directory, safely resolving paths with libpathrs. +import errno import os -import sys import stat -import errno +import sys import flask import flask.json @@ -60,7 +60,7 @@ def get(path): # Permission denied => 403 Forbidden. errno.EACCES: 403, }.get(e.errno, 500) - flask.abort(status_code, "Could not resolve path: %s." % (e,)) + flask.abort(status_code, f"Could not resolve path: {e}.") with handle: try: @@ -69,11 +69,10 @@ def get(path): f, mimetype="application/octet-stream", direct_passthrough=True ) except IsADirectoryError: - with handle.reopen_raw(os.O_RDONLY) as dirf: - with os.scandir(dirf.fileno()) as s: - return flask.json.jsonify( - {dentry.name: json_dentry(dentry) for dentry in s} - ) + with handle.reopen_raw(os.O_RDONLY) as dirf, os.scandir(dirf.fileno()) as s: + return flask.json.jsonify( + {dentry.name: json_dentry(dentry) for dentry in s} + ) def main(root_path=None): From 8e91011440c7d572a91f199d66c85d22d1ca151b Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 2 Sep 2026 14:56:00 +1000 Subject: [PATCH 5/5] gha: add workflow_dispatch trigger Signed-off-by: Aleksa Sarai --- .github/workflows/bindings-c.yml | 1 + .github/workflows/bindings-go.yml | 1 + .github/workflows/bindings-python.yml | 1 + .github/workflows/e2e-tests.yml | 1 + .github/workflows/rust.yml | 1 + 5 files changed, 5 insertions(+) diff --git a/.github/workflows/bindings-c.yml b/.github/workflows/bindings-c.yml index be87ae08..ce5acca4 100644 --- a/.github/workflows/bindings-c.yml +++ b/.github/workflows/bindings-c.yml @@ -19,6 +19,7 @@ on: types: [ published ] schedule: - cron: '0 0 * * *' + workflow_dispatch: name: bindings-c diff --git a/.github/workflows/bindings-go.yml b/.github/workflows/bindings-go.yml index 18471f0e..c59b1723 100644 --- a/.github/workflows/bindings-go.yml +++ b/.github/workflows/bindings-go.yml @@ -19,6 +19,7 @@ on: types: [ published ] schedule: - cron: '0 0 * * *' + workflow_dispatch: name: bindings-go diff --git a/.github/workflows/bindings-python.yml b/.github/workflows/bindings-python.yml index 09065dc4..1df756ef 100644 --- a/.github/workflows/bindings-python.yml +++ b/.github/workflows/bindings-python.yml @@ -19,6 +19,7 @@ on: types: [ published ] schedule: - cron: '0 0 * * *' + workflow_dispatch: env: PYTHON_DIST: ${{ github.workspace }}/.tmp/python3-pathrs-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7f6484af..aa8a0296 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -19,6 +19,7 @@ on: types: [ published ] schedule: - cron: '0 0 * * *' + workflow_dispatch: name: e2e-tests diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 523c92f3..8c006ec8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,6 +19,7 @@ on: types: [ published ] schedule: - cron: '0 0 * * *' + workflow_dispatch: name: rust-ci