Compare commits

...
4 Commits
Author SHA1 Message Date
enricobuehler e1198495e0 Merge pull request 'NixOS+KDE still has no compositor on 0.27.0 — a capped KWin refuses the /proc/exe rung that #158's fix depends on' (#161) from worktree-nixos-capped-compositor-exe-denied into main
apple / swift (push) Successful in 1m32s
ci / web (push) Successful in 1m17s
ci / rust-arm64 (push) Successful in 1m42s
apple / screenshots (push) Successful in 6m3s
ci / bun-nix (push) Successful in 41s
ci / docs-site (push) Successful in 1m26s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 14s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 31s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 19s
deb / build-publish-client-arm64 (push) Successful in 1m43s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 16s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 52s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m24s
windows-host / package (push) Successful in 16m37s
windows-host / winget-source (push) Skipped
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m52s
android / android (push) Successful in 5m46s
deb / build-publish (push) Successful in 4m40s
ci / rust (push) Successful in 7m59s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 10s
arch / build-publish (push) Successful in 9m11s
docker / deploy-docs (push) Successful in 41s
windows-host / canary-manifest (push) Successful in 22s
docker / builders-arm64cross (push) Successful in 18s
deb / build-publish-host (push) Successful in 7m50s
Reviewed-on: #161
2026-08-10 21:28:10 +00:00
enricobuehler 6336e16350 Merge pull request 'bun audit went red on the console — three findings had fixes, the other two have no patched version anywhere' (#160) from worktree-bun-audit-web into main
arch / build-publish (push) Canceled after 32s
audit / cargo-audit (push) Successful in 30s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
audit / bun-audit (plugin-kit) (push) Successful in 19s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
audit / bun-audit (sdk) (push) Successful in 19s
audit / bun-audit (web) (push) Successful in 18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Canceled after 1m11s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
audit / docs-site-audit (push) Successful in 20s
audit / pnpm-audit (push) Successful in 12s
audit / license-gate (push) Successful in 4m42s
nix / flake (push) Successful in 15m20s
Reviewed-on: #160
2026-08-10 21:27:42 +00:00
enricobuehler c817e4bec0 fix(pf-vdisplay): a CAPPED compositor refuses /proc/exe — NixOS+KDE still undetectable
apple / swift (pull_request) Successful in 1m55s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Failing after 3m34s
android / android (pull_request) Successful in 4m27s
ci / rust-arm64 (pull_request) Successful in 1m57s
ci / web (pull_request) Successful in 1m4s
ci / bun-nix (pull_request) Successful in 19s
ci / docs-site (pull_request) Successful in 1m18s
v0.27.0 resolved the nixpkgs wrapper decoration (`.kwin_wayland-w`) through `/proc/<pid>/exe`.
On the box that fix was written for, the kernel refuses that link.

Reading `/proc/<pid>/exe` is not gated on owning the process: `cap_ptrace_access_check` requires
the reader's effective set to be a superset of the target's PERMITTED set. NixOS's own Plasma
module ships `security.wrappers.kwin_wayland = { capabilities = "cap_sys_nice+ep"; }`, and our
host must stay uncapped — a capability is precisely what makes it unidentifiable to KWin (#136).
So the two traps compose: the name NEEDS `exe` because nixpkgs wrapped the binary, and `exe` is
DENIED because NixOS capped it. `detect_active_session` went back to `ActiveKind::None`, and every
connect died `no usable compositor` with `wayland="-"` — the identical signature to the bug #158
fixed, which is why a box worked around with a decoy process broke again when the decoy went away.

`match_name` now falls through to `argv[0]` (`/proc/<pid>/cmdline`) when `exe` is unreadable. It
reads correctly for the same reason `ps` does: make-wrapper's wrapper `exec -a "$0"`s the hidden
binary, so `argv[0]` survives the decoration `comm` does not. It is consulted LAST and never
overrides a readable `exe` — it is the process's own claim about itself, and a same-uid process can
set it to anything; the worst a spoof achieves is aiming detection at a backend that then fails its
own availability probe.

MEASURED (Linux 6.x, same-uid reader, target holding cap_sys_nice), for a file capability and for
the ambient form `security.wrappers` actually uses — identically:

  /proc/<pid> owner   real uid   (so the uid filter upstream was never the problem)
  comm                readable   (decorated + truncated, unusable alone)
  exe                 EACCES
  cmdline (argv[0])   readable

End-to-end against a LIVE capped, `.kwin_wayland-wrapped` process, old resolver vs new, both
extracted verbatim from their own sources: v0.27.0 answers `.kwin_wayland-w` (no match) in both
capped scenarios and `kwin_wayland` uncapped; the fixed one answers `kwin_wayland` in all three.

Also reached by the same rung: gamescope carries `cap_sys_nice` on a number of distros, so a
wrapped-and-capped gamescope was equally invisible to the foreign-gamescope probe.

Tests are fixture-driven (a temp dir with `comm`/`exe`/`cmdline`), for the reason #158 documented:
a renamed `/bin/sleep` stand-in dies instantly on multi-call coreutils and reads exactly like the
resolver being broken. 11 tests extracted verbatim and executed on Linux as a non-root user —
0 failures. `cargo fmt --check` and `scripts/xcheck.sh linux clippy` (`--all-targets`) clean.

Refs #158, #136.
2026-08-10 23:23:59 +02:00
enricobuehler ad806be70a fix(deps): close the dompurify, js-yaml and nanoid advisories in web
ci / rust-arm64 (pull_request) Failing after 5s
ci / web (pull_request) Successful in 1m9s
ci / docs-site (pull_request) Failing after 1m19s
ci / bun-nix (pull_request) Successful in 25s
ci / rust (pull_request) Successful in 6m47s
nix / flake (pull_request) Successful in 13m37s
The audit workflow's blocking `bun audit` leg went red on web with five
findings; sdk and plugin-kit stayed clean. Three had published fixes and are
bumped through the existing overrides block:

  * dompurify  3.4.12 → 3.4.13  GHSA-55q2-fjhq-7xh7 (IN_PLACE hook removal
    leaves a detached subtree executable). The override was already pinned at
    ^3.4.12, which the new advisory range swallowed.
  * js-yaml    4.3.0  → 4.3.1   GHSA-5p4m-2wfm-xmqj (quadratic CPU in !!omap).
  * nanoid     3.3.16 → 3.3.18  GHSA-2v37-7h3g-55p8 (custom generators loop
    forever at size zero). New override — it arrives under postcss, which was
    already pinned, but the pin does not reach postcss's own dependency.

The remaining two are the image-size infinite-loop DoS pair
(GHSA-w3rx-r6r6-pgpr, GHSA-5p2g-fcmc-qvqq), and they are ignored in the
workflow rather than fixed, because they are both unfixable and unreachable:
the vulnerable range is `<= 2.0.2` and 2.0.2 is the latest published version,
so no override can clear them; and image-size rides in under
@unom/ui > @payloadcms/richtext-lexical > ... > payload, where
@payloadcms/richtext-lexical is a peer that only @unom/ui's ./richtext export
needs. The console imports section, toast, button, card, dialog, form/*,
material and tabs — never ./richtext — so payload is auto-installed peer weight
that no bundle and no request path touches. The ignores are scoped to the web
matrix leg so sdk and plugin-kit stay strictly fail-on-any, and the reasoning
is recorded at the step so they can be dropped once image-size ships a fix or
@unom/ui marks that peer optional.

bun.nix is regenerated by the bun2nix postinstall; scripts/ci/check-bun-nix.sh
reports both bun packages in sync. web build, typecheck and server tests pass.
2026-08-10 23:09:57 +02:00
7 changed files with 232 additions and 31 deletions
+20 -1
View File
@@ -91,8 +91,27 @@ jobs:
# advisory, the same fail-on-vulnerability stance as cargo-audit above; triage a finding by # advisory, the same fail-on-vulnerability stance as cargo-audit above; triage a finding by
# bumping the dep (or, if genuinely unfixable + inapplicable, pinning a resolution and # bumping the dep (or, if genuinely unfixable + inapplicable, pinning a resolution and
# noting why here). # noting why here).
#
# web carries two ignores, the ONLY ones in a blocking tree — both image-size advisories
# (GHSA-w3rx-r6r6-pgpr ICNS, GHSA-5p2g-fcmc-qvqq JXL/HEIF infinite-loop DoS). They are
# unfixable AND unreachable:
# * unfixable — the vulnerable range is `<= 2.0.2` and 2.0.2 IS latest; upstream has
# published no patched release, so no override can clear them.
# * unreachable — image-size rides in under `@unom/ui @payloadcms/richtext-lexical
# … payload`, and @payloadcms/richtext-lexical is a PEER of @unom/ui that only its
# `./richtext` export needs. The console imports section/toast/button/card/dialog/
# form/*/material/tabs — never `./richtext` — so payload is auto-installed peer weight
# that no bundle, and no request path, ever touches.
# Drop these the moment image-size ships a fix, or @unom/ui marks that peer optional
# (peerDependenciesMeta) and the chain leaves web/bun.lock entirely — either one makes the
# bare `bun audit` green again. Scoped per-tree so sdk/plugin-kit stay strictly fail-on-any.
- name: bun audit - name: bun audit
run: bun audit run: |
if [ "${{ matrix.tree }}" = "web" ]; then
bun audit --ignore=GHSA-w3rx-r6r6-pgpr --ignore=GHSA-5p2g-fcmc-qvqq
else
bun audit
fi
# Kept OUT of the bun-audit matrix so this tree's known-advisory state can't normalize failure # Kept OUT of the bun-audit matrix so this tree's known-advisory state can't normalize failure
# in a shipping tree. Non-blocking via a step-level `||` (NOT job-level continue-on-error, which # in a shipping tree. Non-blocking via a step-level `||` (NOT job-level continue-on-error, which
+30
View File
@@ -12,6 +12,36 @@ with the version table of the release you are moving to, then read **Breaking ch
--- ---
## v0.27.1 — in development
### NixOS + KDE — session detection, the other half
🛑 **v0.27.0's NixOS session-detection fix did not reach a stock NixOS + Plasma 6 box.** It resolved
the nixpkgs wrapper decoration through `/proc/<pid>/exe` (below) — and on that exact box the kernel
refuses to let us read that link. Reading `/proc/<pid>/exe` is not gated on owning the process: it
goes through `cap_ptrace_access_check`, which requires the reader's effective set to be a superset
of the target's **permitted** set. NixOS's own Plasma module ships
`security.wrappers.kwin_wayland = { capabilities = "cap_sys_nice+ep"; }`, so KWin holds a capability
and the host — which must stay uncapped, because a capability is exactly what makes it
unidentifiable to KWin (v0.27.0, above) — gets `EACCES`. The two traps compose: the name *needs*
`exe` because nixpkgs wrapped the binary, and `exe` is *denied* because NixOS capped it. Detection
went straight back to `ActiveKind::None`, `wayland` to `-`, and every connect to
`no usable compositor`. It presents identically to the v0.27.0 bug, which is why a box that had been
worked around with a decoy process broke again the moment the decoy was removed.
Name resolution now falls through to `argv[0]` (`/proc/<pid>/cmdline`) when the kernel refuses `exe`.
That reads correctly for the same reason `ps` does: make-wrapper's wrapper `exec -a "$0"`s the hidden
binary, so `argv[0]` survives the decoration `comm` does not. Measured on Linux 6.x against a capped
target, for a file capability and for the ambient form `security.wrappers` uses, identically: the
`/proc/<pid>` directory keeps its real owner (so the uid filter was never the problem), `comm` and
`cmdline` stay readable, and only `exe` fails. `argv[0]` is consulted **last** and never overrides a
readable `exe` — it is the process's own claim about itself, and a same-uid process can set it to
anything; the worst a spoof achieves is aiming detection at a backend that then fails its own
availability probe. The `comm` fast path is still one read for every ordinary distro.
Also reached by the same rung: `gamescope` carries `cap_sys_nice` on a number of distros, so a
*wrapped and capped* gamescope was equally invisible to the foreign-gamescope probe.
## v0.27.0 ## v0.27.0
87 commits since v0.26.0. 87 commits since v0.26.0.
+159 -13
View File
@@ -135,7 +135,37 @@ const COMM_MAX: usize = 15;
/// ///
/// The `comm` fast path is kept for every ordinary distro: one read, no readlink. Only a name that /// The `comm` fast path is kept for every ordinary distro: one read, no readlink. Only a name that
/// *could* be decorated or truncated — it starts with `.`, or it is exactly [`COMM_MAX`] bytes — /// *could* be decorated or truncated — it starts with `.`, or it is exactly [`COMM_MAX`] bytes —
/// is re-resolved through `/proc/<pid>/exe`, which carries the full, untruncated file name. /// is re-resolved, first through `/proc/<pid>/exe` and then, when the kernel refuses that link,
/// through `argv[0]`.
///
/// 🛑 **That last rung is not defensive padding — without it this resolver misses the exact box it
/// was written for.** Reading `/proc/<pid>/exe` is *not* merely a matter of owning the process: the
/// kernel gates it behind `cap_ptrace_access_check`, which demands the reader's effective set be a
/// superset of the target's PERMITTED set. A compositor holding a capability is therefore opaque to
/// our (deliberately uncapped — see the KWin identification note in `pf-encode`) host, same uid or
/// not. And NixOS's own Plasma module ships exactly that:
/// `security.wrappers.kwin_wayland = { capabilities = "cap_sys_nice+ep"; }`. So on NixOS + KDE the
/// two traps compose — the name needs `exe` *because* nixpkgs wrapped it, and `exe` is denied
/// *because* NixOS capped it — and the session probe went straight back to
/// [`crate::ActiveKind::None`] on a running desktop.
///
/// Measured (Linux 6.x, same-uid reader, target holding `cap_sys_nice`), for a file capability and
/// for the ambient-capability form `security.wrappers` actually uses, identically:
///
/// | probe | capped target |
/// |---|---|
/// | `/proc/<pid>` owner | ✅ still the real uid — the uid filter upstream is unaffected |
/// | `comm` | ✅ readable (decorated/truncated, so still unusable on its own) |
/// | `exe` | ❌ **EACCES** |
/// | `cmdline` (`argv[0]`) | ✅ readable |
///
/// `argv[0]` is only consulted when the kernel has refused the authoritative answer, because it is
/// the process's own claim about itself rather than the kernel's: a same-uid process can set it to
/// anything. The exposure that buys is small and one-directional — the worst a spoof achieves is
/// aiming detection at a compositor backend that then fails its own availability probe — whereas
/// without the rung a capped compositor is simply invisible. It reads correctly here for the same
/// reason `ps` does: make-wrapper's generated wrapper `exec -a "$0"`s the hidden binary, so
/// `argv[0]` survives the decoration that `comm` does not.
/// ///
/// `pid_path` is a `/proc/<pid>` directory. `None` when the process vanished mid-scan. /// `pid_path` is a `/proc/<pid>` directory. `None` when the process vanished mid-scan.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -146,20 +176,48 @@ pub(crate) fn match_name(pid_path: &std::path::Path) -> Option<String> {
if !comm.starts_with('.') && comm.len() < COMM_MAX { if !comm.starts_with('.') && comm.len() < COMM_MAX {
return Some(comm.to_string()); return Some(comm.to_string());
} }
// Reading our OWN uid's `/proc/<pid>/exe` needs no privilege (every caller filters on uid // The authoritative rung: the kernel's own record of the executed file, untruncated. Absent for
// first), but it is still absent for a kernel thread and for a process exiting under us // a kernel thread and for a process exiting under us, and REFUSED for a capability-holding one.
// in which case the truncated `comm` is the best that exists. let exe = std::fs::read_link(pid_path.join("exe")).ok();
match std::fs::read_link(pid_path.join("exe")) if let Some(full) = exe
.ok()
.as_deref() .as_deref()
.and_then(|p| p.file_name()) .and_then(|p| p.file_name())
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
{ {
Some(full) => Some(undecorate(full).to_string()), return Some(undecorate(full).to_string());
}
// Refused or gone: fall back to what the process calls itself, then to the truncated `comm`.
match argv0_name(pid_path) {
Some(name) => {
tracing::debug!(
comm = %comm,
resolved = %name,
"/proc/<pid>/exe unreadable (a capability-holding process refuses it); \
identified via argv[0]"
);
Some(name)
}
None => Some(comm.to_string()), None => Some(comm.to_string()),
} }
} }
/// The file name in `argv[0]`, with nixpkgs decoration undone — the last rung of [`match_name`].
///
/// `/proc/<pid>/cmdline` is NUL-separated, so the first field is `argv[0]` whole, with no splitting
/// on whitespace to get wrong. `None` when it is unreadable or empty, which is the normal state for
/// a kernel thread and for a zombie.
#[cfg(target_os = "linux")]
fn argv0_name(pid_path: &std::path::Path) -> Option<String> {
let raw = std::fs::read(pid_path.join("cmdline")).ok()?;
let argv0 = raw.split(|b| *b == 0).next()?;
// A process that rewrote its own argv (setproctitle-style) can leave anything here, including
// something that is not a path at all — `file_name` simply yields it unchanged and it fails to
// match any compositor name, which is the correct outcome.
let argv0 = std::str::from_utf8(argv0).ok()?;
let name = std::path::Path::new(argv0).file_name()?.to_str()?;
(!name.is_empty()).then(|| undecorate(name).to_string())
}
/// Strip nixpkgs `wrapProgram` decoration: `.<name>-wrapped`, plus the `_` suffixes make-wrapper /// Strip nixpkgs `wrapProgram` decoration: `.<name>-wrapped`, plus the `_` suffixes make-wrapper
/// appends when that hidden name is already taken (a doubly-wrapped app — Qt *and* GApps). /// appends when that hidden name is already taken (a doubly-wrapped app — Qt *and* GApps).
/// ///
@@ -366,8 +424,12 @@ mod name_tests {
use super::*; use super::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
/// A fake `/proc/<pid>` directory: a `comm` file and, optionally, the `exe` symlink. Removed on /// A fake `/proc/<pid>` directory: a `comm` file and, optionally, the `exe` symlink and a
/// drop. /// `cmdline`. Removed on drop.
///
/// An absent `exe` stands in for **both** ways the real link yields nothing: a process exiting
/// under the scan, and — the case that matters here — a capability-holding one, whose link the
/// kernel refuses with EACCES. `match_name` cannot tell those apart and does not need to.
struct FakePid { struct FakePid {
dir: PathBuf, dir: PathBuf,
} }
@@ -375,6 +437,12 @@ mod name_tests {
impl FakePid { impl FakePid {
/// `comm` is written exactly as the kernel would report it — i.e. already truncated. /// `comm` is written exactly as the kernel would report it — i.e. already truncated.
fn new(tag: &str, comm: &str, exe: Option<&str>) -> FakePid { fn new(tag: &str, comm: &str, exe: Option<&str>) -> FakePid {
FakePid::with_cmdline(tag, comm, exe, None)
}
/// `cmdline` is the NUL-separated argument vector the kernel exposes; the fixture is given
/// just `argv[0]` and appends the terminator, as a real one carries.
fn with_cmdline(tag: &str, comm: &str, exe: Option<&str>, argv0: Option<&str>) -> FakePid {
let dir = std::env::temp_dir().join(format!("pf-vd-name-{tag}-{}", std::process::id())); let dir = std::env::temp_dir().join(format!("pf-vd-name-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("fixture dir"); std::fs::create_dir_all(&dir).expect("fixture dir");
@@ -388,6 +456,9 @@ mod name_tests {
) )
.expect("exe symlink"); .expect("exe symlink");
} }
if let Some(argv0) = argv0 {
std::fs::write(dir.join("cmdline"), format!("{argv0}\0--session\0")).expect("cmd");
}
FakePid { dir } FakePid { dir }
} }
fn path(&self) -> &Path { fn path(&self) -> &Path {
@@ -505,14 +576,87 @@ mod name_tests {
assert_eq!(match_name(p.path()).as_deref(), Some("kwin_wayland")); assert_eq!(match_name(p.path()).as_deref(), Some("kwin_wayland"));
} }
/// A decorated-or-truncated name whose `exe` cannot be read (a kernel thread, or a process /// A decorated-or-truncated name with neither `exe` nor `cmdline` to fall back on (a kernel
/// exiting under the scan) degrades to the truncated `comm` instead of failing the whole entry. /// thread, or a process exiting under the scan) degrades to the truncated `comm` instead of
/// failing the whole entry.
#[test] #[test]
fn an_unreadable_exe_falls_back_to_comm() { fn an_unreadable_exe_falls_back_to_comm() {
let p = FakePid::new("noexe", ".kwin_wayland-w", None); let p = FakePid::new("noexe", ".kwin_wayland-w", None);
assert_eq!(match_name(p.path()).as_deref(), Some(".kwin_wayland-w")); assert_eq!(match_name(p.path()).as_deref(), Some(".kwin_wayland-w"));
} }
/// **The NixOS + KDE field bug in one assertion.** nixpkgs wraps the binary, so `comm` is
/// `.kwin_wayland-w` and only `exe` carries the real name — and NixOS's own Plasma module hands
/// KWin `cap_sys_nice+ep` through `security.wrappers`, so the kernel refuses that link to our
/// uncapped host. Both traps at once is not a hypothetical combination: it is the default
/// install. `argv[0]` is what survives, because make-wrapper's wrapper `exec -a "$0"`s the
/// hidden binary.
#[test]
fn a_capped_wrapped_compositor_is_identified_by_argv0() {
for (tag, comm, argv0, want) in [
// Plasma's own startup execs the wrapper by absolute path.
(
"capkwin",
".kwin_wayland-w",
"/run/wrappers/bin/kwin_wayland",
"kwin_wayland",
),
// …and a bare name is just as ordinary.
("capbare", ".kwin_wayland-w", "kwin_wayland", "kwin_wayland"),
// gamescope carries `cap_sys_nice` on a great many distros, wrapped or not.
(
"capgame",
".gamescope-wrap",
"/nix/store/aaaa-gamescope/bin/gamescope",
"gamescope",
),
// A wrapper that passes the hidden path through as `argv[0]` still undecorates.
(
"capraw",
".kwin_wayland-w",
"/nix/store/eeee-kwin/bin/.kwin_wayland-wrapped",
"kwin_wayland",
),
] {
let p = FakePid::with_cmdline(tag, comm, None, Some(argv0));
assert_eq!(
match_name(p.path()).as_deref(),
Some(want),
"a capped, wrapped {want} must still be identified from argv[0]"
);
}
}
/// `exe` outranks `argv[0]` whenever the kernel allows it: `argv[0]` is the process's own claim
/// about itself and a same-uid process can set it to anything, so it may never override the
/// kernel's answer — only stand in when there is none.
#[test]
fn a_readable_exe_outranks_a_lying_argv0() {
let p = FakePid::with_cmdline(
"liar",
".gamescope-wrap",
Some(".gamescope-wrapped"),
Some("kwin_wayland"),
);
assert_eq!(match_name(p.path()).as_deref(), Some("gamescope"));
}
/// A zombie's `cmdline` is empty, and `argv[0]` can be an empty string even when it is not —
/// neither may yield an empty name (which would then be compared against, and could match, a
/// compositor name only by accident).
#[test]
fn an_empty_cmdline_does_not_produce_a_name() {
for (tag, cmdline) in [("zombie", ""), ("nulls", "\0\0")] {
let dir = std::env::temp_dir().join(format!("pf-vd-name-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("fixture dir");
std::fs::write(dir.join("comm"), ".kwin_wayland-w\n").expect("comm");
std::fs::write(dir.join("cmdline"), cmdline).expect("cmdline");
assert_eq!(match_name(&dir).as_deref(), Some(".kwin_wayland-w"));
let _ = std::fs::remove_dir_all(&dir);
}
}
/// A pid directory that does not exist yields `None`, not a bogus name — the scans `continue`. /// A pid directory that does not exist yields `None`, not a bogus name — the scans `continue`.
#[test] #[test]
fn a_vanished_process_yields_none() { fn a_vanished_process_yields_none() {
@@ -520,8 +664,10 @@ mod name_tests {
} }
/// The one thing a fixture cannot establish: that reading `/proc/<pid>/exe` is actually /// The one thing a fixture cannot establish: that reading `/proc/<pid>/exe` is actually
/// *permitted* for a process of our own uid, which the whole resolver depends on. Checked /// *permitted* for a process of our own uid. Checked against the only such process guaranteed
/// against the only such process guaranteed to be running — this one. /// to be running — this one. ⚠ It holds because *we* are uncapped, and says nothing about the
/// processes being scanned: a capped target refuses this same link, which is what
/// [`match_name`]'s `argv[0]` rung exists for.
#[test] #[test]
fn our_own_exe_link_is_readable() { fn our_own_exe_link_is_readable() {
let me = Path::new("/proc/self"); let me = Path::new("/proc/self");
+5 -1
View File
@@ -315,7 +315,11 @@ pub fn detect_active_session() -> ActiveSession {
// exactly, `pkill -x` style — but resolved through [`crate::proc::match_name`], NOT a raw // exactly, `pkill -x` style — but resolved through [`crate::proc::match_name`], NOT a raw
// `comm` read: on NixOS every one of these binaries is a nixpkgs wrapper whose real ELF is // `comm` read: on NixOS every one of these binaries is a nixpkgs wrapper whose real ELF is
// `.<name>-wrapped`, so a raw `comm` says `.kwin_wayland-w` and this whole probe answered // `.<name>-wrapped`, so a raw `comm` says `.kwin_wayland-w` and this whole probe answered
// `None` on a running KDE desktop. // `None` on a running KDE desktop. ⚠ Nor is `/proc/<pid>/exe` alone enough to undo that: NixOS
// caps KWin (`security.wrappers.kwin_wayland`, `cap_sys_nice+ep`) and the kernel refuses that
// link to an uncapped reader — see `match_name`, which falls through to `argv[0]` for exactly
// this box. The uid filter below is unaffected: a capped process's `/proc/<pid>` keeps its
// real owner (measured).
let mut kind = ActiveKind::None; let mut kind = ActiveKind::None;
let mut best = 0u8; let mut best = 0u8;
// The winning compositor's PID — kept so a same-kind compositor RESTART (a new PID) bumps the // The winning compositor's PID — kept so a same-kind compositor RESTART (a new PID) bumps the
+6 -5
View File
@@ -47,11 +47,12 @@
}, },
"overrides": { "overrides": {
"brace-expansion": "^5.0.9", "brace-expansion": "^5.0.9",
"dompurify": "^3.4.12", "dompurify": "^3.4.13",
"fast-uri": "^3.1.5", "fast-uri": "^3.1.5",
"immutable": "^4.3.9", "immutable": "^4.3.9",
"js-yaml": "^4.3.0", "js-yaml": "^4.3.1",
"linkify-it": "^5.0.2", "linkify-it": "^5.0.2",
"nanoid": "^3.3.18",
"postcss": "^8.5.25", "postcss": "^8.5.25",
"sharp": "^0.35.3", "sharp": "^0.35.3",
"tar": "^7.5.21", "tar": "^7.5.21",
@@ -1304,7 +1305,7 @@
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="], "dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="],
"dot-prop": ["dot-prop@10.2.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw=="], "dot-prop": ["dot-prop@10.2.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw=="],
@@ -1580,7 +1581,7 @@
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -1774,7 +1775,7 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
"next": ["next@16.2.11", "", { "dependencies": { "@next/env": "16.2.11", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.11", "@next/swc-darwin-x64": "16.2.11", "@next/swc-linux-arm64-gnu": "16.2.11", "@next/swc-linux-arm64-musl": "16.2.11", "@next/swc-linux-x64-gnu": "16.2.11", "@next/swc-linux-x64-musl": "16.2.11", "@next/swc-win32-arm64-msvc": "16.2.11", "@next/swc-win32-x64-msvc": "16.2.11", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ=="], "next": ["next@16.2.11", "", { "dependencies": { "@next/env": "16.2.11", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.11", "@next/swc-darwin-x64": "16.2.11", "@next/swc-linux-arm64-gnu": "16.2.11", "@next/swc-linux-arm64-musl": "16.2.11", "@next/swc-linux-x64-gnu": "16.2.11", "@next/swc-linux-x64-musl": "16.2.11", "@next/swc-win32-arm64-msvc": "16.2.11", "@next/swc-win32-x64-msvc": "16.2.11", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ=="],
+9 -9
View File
@@ -2619,9 +2619,9 @@
url = "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz"; url = "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz";
hash = "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="; hash = "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==";
}; };
"dompurify@3.4.12" = fetchurl { "dompurify@3.4.13" = fetchurl {
url = "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz"; url = "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz";
hash = "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="; hash = "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==";
}; };
"dot-prop@10.2.0" = fetchurl { "dot-prop@10.2.0" = fetchurl {
url = "https://registry.npmjs.org/dot-prop/-/dot-prop-10.2.0.tgz"; url = "https://registry.npmjs.org/dot-prop/-/dot-prop-10.2.0.tgz";
@@ -3219,9 +3219,9 @@
url = "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz"; url = "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz";
hash = "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="; hash = "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==";
}; };
"js-yaml@4.3.0" = fetchurl { "js-yaml@4.3.1" = fetchurl {
url = "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz"; url = "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz";
hash = "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="; hash = "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==";
}; };
"jsesc@3.1.0" = fetchurl { "jsesc@3.1.0" = fetchurl {
url = "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz"; url = "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz";
@@ -3671,9 +3671,9 @@
url = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"; url = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz";
hash = "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="; hash = "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==";
}; };
"nanoid@3.3.16" = fetchurl { "nanoid@3.3.18" = fetchurl {
url = "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz"; url = "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz";
hash = "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="; hash = "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==";
}; };
"napi-wasm@1.1.3" = fetchurl { "napi-wasm@1.1.3" = fetchurl {
url = "https://registry.npmjs.org/napi-wasm/-/napi-wasm-1.1.3.tgz"; url = "https://registry.npmjs.org/napi-wasm/-/napi-wasm-1.1.3.tgz";
+3 -2
View File
@@ -62,14 +62,15 @@
}, },
"overrides": { "overrides": {
"tar": "^7.5.21", "tar": "^7.5.21",
"dompurify": "^3.4.12", "dompurify": "^3.4.13",
"linkify-it": "^5.0.2", "linkify-it": "^5.0.2",
"sharp": "^0.35.3", "sharp": "^0.35.3",
"fast-uri": "^3.1.5", "fast-uri": "^3.1.5",
"immutable": "^4.3.9", "immutable": "^4.3.9",
"undici": "^7.29.0", "undici": "^7.29.0",
"postcss": "^8.5.25", "postcss": "^8.5.25",
"js-yaml": "^4.3.0", "nanoid": "^3.3.18",
"js-yaml": "^4.3.1",
"brace-expansion": "^5.0.9" "brace-expansion": "^5.0.9"
} }
} }