From 2b33f20ba920ea474ce8ad522c4dd7d77dbb8c11 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 11 May 2026 13:07:50 +0200 Subject: [PATCH 1/8] fix sudo handling and pkgx-under-/root reachability Restore the privilege drop when running as root via sudo (so pkgx caches stay user-owned), but only when the resolved pkgx binary is actually reachable from $SUDO_USER. Falls back to running pkgx as root rather than crashing the inner sudo with "Permission denied" when pkgx lives under /root/.pkgx/ (pkgxdev/pkgm#68). - Restore drop-privilege behaviour for `sudo pkgm` (fixes the regression flagged by jhheider on #83). - Resolve an alternative pkgx under $SUDO_USER's home / /usr/local when the current path is unreachable to $SUDO_USER. - Override HOME so pkgx caches under the invoking user's tree. - Stop mutating `args` so the args[0] lookup at line 341 keeps working. - Avoid the `Deno.env.get("USER")!` non-null assertion crash. - Call install_prefix() once (it has filesystem side effects). - Keep the visible log surface unchanged: only the pre-existing "installing as root" warning fires by default; the new "pkgx unreachable" diagnostic is gated behind PKGM_DEBUG. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkgm.ts | 94 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/pkgm.ts b/pkgm.ts index f3f08c3..53dc13f 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -297,25 +297,43 @@ async function query_pkgx( set("PKGX_DIST_URL"); set("XDG_DATA_HOME"); - const needs_sudo_backwards = install_prefix().string == "/usr/local"; - let cmd = needs_sudo_backwards ? "/usr/bin/sudo" : pkgx; - if (needs_sudo_backwards) { - if (!Deno.env.get("SUDO_USER")) { - if (Deno.uid() == 0) { + const isRoot = Deno.uid() == 0; + const sudoUser = Deno.env.get("SUDO_USER"); + const prefix = install_prefix().string; + const isSystemPrefix = prefix == "/usr/local"; + + let cmd = pkgx; + let cmd_args = args; + + if (isSystemPrefix) { + if (isRoot && sudoUser) { + // Drop privileges so pkgx writes its cache as the invoking user, not root. + // But only if pkgx is reachable from sudoUser — otherwise the inner sudo + // aborts with "unable to execute …: Permission denied" (pkgxdev/pkgm#68). + const reachable = pkgx_reachable_as(pkgx, sudoUser); + if (reachable) { + cmd = "/usr/bin/sudo"; + cmd_args = ["-u", sudoUser, reachable, ...args]; + // Override HOME, or pkgx will cache back under /root/ where sudoUser + // can't reach it on the next invocation. + const home = user_home(sudoUser); + if (home) env.HOME = home; + } else if (Deno.env.get("PKGM_DEBUG")) { console.error( - "%cwarning", - "color:yellow", - "installing as root; installing via `sudo` is preferred", + `pkgm: \`pkgx\` at ${pkgx} is not reachable as ${sudoUser}; running it as root`, ); } - cmd = pkgx; - } else { - args.unshift("-u", Deno.env.get("SUDO_USER")!, pkgx); + } else if (isRoot) { + console.error( + "%cwarning", + "color:yellow", + "installing as root; installing via `sudo` is preferred", + ); } } const proc = new Deno.Command(cmd, { - args: [...args, "--json=v1"], + args: [...cmd_args, "--json=v1"], stdout: "piped", env, clearEnv: true, @@ -766,6 +784,58 @@ function install_prefix() { } } +function user_home(user: string): string | undefined { + // getent is the portable lookup on Linux; on macOS getent is absent but the + // /root/.pkgx scenario this guards against doesn't arise there in practice. + try { + const out = new Deno.Command("/usr/bin/getent", { + args: ["passwd", user], + }).outputSync(); + if (!out.success) return undefined; + const fields = new TextDecoder().decode(out.stdout).trim().split(":"); + return fields[5] || undefined; + } catch { + return undefined; + } +} + +function pkgx_reachable_as(current: string, user: string): string | undefined { + if (reachable_as(current, user)) return current; + + const home = user_home(user); + if (home) { + // Versioned pkgx.sh layout: ~/.pkgx/pkgx.sh/v/bin/pkgx — pick the highest. + const root = join(home, ".pkgx/pkgx.sh"); + if (existsSync(root)) { + let best: { v: SemVer; path: string } | undefined; + for (const entry of Deno.readDirSync(root)) { + if (!entry.isDirectory || !entry.name.startsWith("v")) continue; + try { + const v = new SemVer(entry.name.slice(1)); + const path = join(root, entry.name, "bin/pkgx"); + if (!existsSync(path)) continue; + if (!best || v.gt(best.v)) best = { v, path }; + } catch { /* skip malformed version dir */ } + } + if (best) return best.path; + } + const local = join(home, ".local/bin/pkgx"); + if (existsSync(local)) return local; + } + if (existsSync("/usr/local/bin/pkgx")) return "/usr/local/bin/pkgx"; + return undefined; +} + +function reachable_as(p: string, user: string): boolean { + // Conservative heuristic: private home dirs are typically mode 700, so a + // path under another user's home is unreachable. System paths and the + // user's own home are assumed reachable. + if (p.startsWith("/root/")) return user === "root"; + const m = p.match(/^\/home\/([^/]+)\//); + if (m) return m[1] === user; + return true; +} + function dev_stub_text(selfpath: string, bin_prefix: string, name: string) { if (selfpath.startsWith("/usr/local") && selfpath != "/usr/local/bin/dev") { return ` From caa7efcbfb32c837b62198a285b0a5cc10d23572 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 11 May 2026 19:25:29 +0200 Subject: [PATCH 2/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkgm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgm.ts b/pkgm.ts index 53dc13f..28509c3 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -788,7 +788,7 @@ function user_home(user: string): string | undefined { // getent is the portable lookup on Linux; on macOS getent is absent but the // /root/.pkgx scenario this guards against doesn't arise there in practice. try { - const out = new Deno.Command("/usr/bin/getent", { + const out = new Deno.Command("getent", { args: ["passwd", user], }).outputSync(); if (!out.success) return undefined; From 7c15a3e0935fc9156f16fd4178a18e2180c9f9d8 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 11 May 2026 19:39:39 +0200 Subject: [PATCH 3/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkgm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgm.ts b/pkgm.ts index 28509c3..aec2107 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -313,7 +313,7 @@ async function query_pkgx( const reachable = pkgx_reachable_as(pkgx, sudoUser); if (reachable) { cmd = "/usr/bin/sudo"; - cmd_args = ["-u", sudoUser, reachable, ...args]; + cmd_args = ["-u", sudoUser, "--", reachable, ...args]; // Override HOME, or pkgx will cache back under /root/ where sudoUser // can't reach it on the next invocation. const home = user_home(sudoUser); From 12ad751863a26019bd20b90f378663a61214806b Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 11 May 2026 20:46:35 +0200 Subject: [PATCH 4/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkgm.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkgm.ts b/pkgm.ts index aec2107..0db4627 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -787,8 +787,12 @@ function install_prefix() { function user_home(user: string): string | undefined { // getent is the portable lookup on Linux; on macOS getent is absent but the // /root/.pkgx scenario this guards against doesn't arise there in practice. + const getent = existsSync("/usr/bin/getent") + ? "/usr/bin/getent" + : "/bin/getent"; + try { - const out = new Deno.Command("getent", { + const out = new Deno.Command(getent, { args: ["passwd", user], }).outputSync(); if (!out.success) return undefined; From 00fdfccf615f87c2129972014d7c8a5bdbe8a628 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 11 May 2026 21:30:53 +0200 Subject: [PATCH 5/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkgm.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pkgm.ts b/pkgm.ts index 0db4627..a6fe038 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -812,14 +812,20 @@ function pkgx_reachable_as(current: string, user: string): string | undefined { const root = join(home, ".pkgx/pkgx.sh"); if (existsSync(root)) { let best: { v: SemVer; path: string } | undefined; - for (const entry of Deno.readDirSync(root)) { - if (!entry.isDirectory || !entry.name.startsWith("v")) continue; - try { - const v = new SemVer(entry.name.slice(1)); - const path = join(root, entry.name, "bin/pkgx"); - if (!existsSync(path)) continue; - if (!best || v.gt(best.v)) best = { v, path }; - } catch { /* skip malformed version dir */ } + try { + if (Deno.statSync(root).isDirectory) { + for (const entry of Deno.readDirSync(root)) { + if (!entry.isDirectory || !entry.name.startsWith("v")) continue; + try { + const v = new SemVer(entry.name.slice(1)); + const path = join(root, entry.name, "bin/pkgx"); + if (!existsSync(path)) continue; + if (!best || v.gt(best.v)) best = { v, path }; + } catch { /* skip malformed version dir */ } + } + } + } catch { + // Ignore unreadable/non-directory pkgx.sh roots and fall back to other locations. } if (best) return best.path; } From 8f45c964efb312466aad2d3de3804f2b7f5e6e75 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 11 May 2026 22:42:10 +0200 Subject: [PATCH 6/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkgm.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/pkgm.ts b/pkgm.ts index a6fe038..c8dae68 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -784,25 +784,68 @@ function install_prefix() { } } -function user_home(user: string): string | undefined { - // getent is the portable lookup on Linux; on macOS getent is absent but the - // /root/.pkgx scenario this guards against doesn't arise there in practice. - const getent = existsSync("/usr/bin/getent") - ? "/usr/bin/getent" - : "/bin/getent"; +function user_home_from_passwd(user: string): string | undefined { + try { + const passwd = Deno.readTextFileSync("/etc/passwd"); + for (const line of passwd.split("\n")) { + if (!line || line.startsWith("#")) continue; + const fields = line.split(":"); + if (fields[0] === user) return fields[5] || undefined; + } + } catch { + // Ignore unreadable or absent passwd database and fall back to other lookups. + } + + return undefined; +} + +function user_home_from_dscl(user: string): string | undefined { + if (!existsSync("/usr/bin/dscl")) return undefined; try { - const out = new Deno.Command(getent, { - args: ["passwd", user], + const out = new Deno.Command("/usr/bin/dscl", { + args: [".", "-read", `/Users/${user}`, "NFSHomeDirectory"], }).outputSync(); if (!out.success) return undefined; - const fields = new TextDecoder().decode(out.stdout).trim().split(":"); - return fields[5] || undefined; + + const line = new TextDecoder().decode(out.stdout).trim(); + const prefix = "NFSHomeDirectory:"; + if (!line.startsWith(prefix)) return undefined; + + const home = line.slice(prefix.length).trim(); + return home || undefined; } catch { return undefined; } } +function user_home(user: string): string | undefined { + // Prefer getent where available, but fall back to passwd parsing and macOS + // dscl so HOME can still be resolved when dropping privileges on systems + // without getent. + const getent = existsSync("/usr/bin/getent") + ? "/usr/bin/getent" + : existsSync("/bin/getent") + ? "/bin/getent" + : undefined; + + if (getent) { + try { + const out = new Deno.Command(getent, { + args: ["passwd", user], + }).outputSync(); + if (out.success) { + const fields = new TextDecoder().decode(out.stdout).trim().split(":"); + if (fields[5]) return fields[5]; + } + } catch { + // Ignore getent lookup failures and try portable fallbacks below. + } + } + + return user_home_from_passwd(user) ?? user_home_from_dscl(user); +} + function pkgx_reachable_as(current: string, user: string): string | undefined { if (reachable_as(current, user)) return current; From 28df252ca375c63cc2cd837e0d5c1bfa0a2eae99 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Tue, 12 May 2026 08:07:08 +0200 Subject: [PATCH 7/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkgm.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkgm.ts b/pkgm.ts index c8dae68..41ee018 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -883,9 +883,15 @@ function reachable_as(p: string, user: string): boolean { // Conservative heuristic: private home dirs are typically mode 700, so a // path under another user's home is unreachable. System paths and the // user's own home are assumed reachable. - if (p.startsWith("/root/")) return user === "root"; - const m = p.match(/^\/home\/([^/]+)\//); - if (m) return m[1] === user; + const home = user_home(user); + if (home && (p === home || p.startsWith(`${home}/`))) return true; + + if (p === "/root" || p.startsWith("/root/")) return false; + if (p === "/var/root" || p.startsWith("/var/root/")) return false; + + const m = p.match(/^\/(home|Users)\/([^/]+)(?:\/|$)/); + if (m) return false; + return true; } From 54cd27dc84c6da6221db7d7066ec046cf85ce81f Mon Sep 17 00:00:00 2001 From: tannevaled Date: Tue, 12 May 2026 08:15:59 +0200 Subject: [PATCH 8/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkgm.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgm.ts b/pkgm.ts index 41ee018..d5d7669 100755 --- a/pkgm.ts +++ b/pkgm.ts @@ -889,8 +889,7 @@ function reachable_as(p: string, user: string): boolean { if (p === "/root" || p.startsWith("/root/")) return false; if (p === "/var/root" || p.startsWith("/var/root/")) return false; - const m = p.match(/^\/(home|Users)\/([^/]+)(?:\/|$)/); - if (m) return false; + if (p.match(/^\/(home|Users)\/([^/]+)(?:\/|$)/)) return false; return true; }