Privilege escalation and sudo

Most Linux package managers need sudo to perform system-wide operations, and on other OSes you may be prompted for your password to install privileged payloads (a macOS cask shipping a kernel extension or a .pkg installer, for example):

$ brew install --cask macfuse
==> Caveats
macfuse requires a kernel extension to work.
If the installation fails, retry after you enable it in:
  System Preferences → Security & Privacy → General

For more information, refer to vendor documentation or this Apple Technical Note:
  https://developer.apple.com/library/content/technotes/tn2459/_index.html

==> Downloading https://github.com/osxfuse/osxfuse/releases/download/macfuse-4.2.5/macfuse-4.2.5.dmg
Already downloaded: /Users/kde/Library/Caches/Homebrew/downloads/d7961d772f16bad95962f1a780b545a5dbb4788ec6e1ec757994bb5296397b1c--macfuse-4.2.5.dmg
==> Installing Cask macfuse
==> Running installer for macfuse; your password may be necessary.
Package installers may write to any location; options such as `--appdir` are ignored.
Password:

Escalation policy

Which managers escalate is decided per manager. System package managers (apt, dnf, pacman, zypper, xbps, macports, snap, and the like) run their state-changing operations through sudo by default; user-level managers (brew, npm, pip, …) do not, and daemon-backed managers authorizing through polkit (flatpak, fwupd, pkcon) need no wrap at all. A password prompt raised by those last ones comes from polkit, not sudo, so a NOPASSWD rule does not silence it: grant a polkit rule instead.

Choosing the escalator

mpm drives sudo, doas or run0, picking whichever the host carries and preferring them in that order. That matters on the systems that ship no sudo at all: OpenBSD replaced it with doas in its base system, and neither Alpine nor NetBSD carries one by default, while mpm wraps escalating managers on all three (apk, pkg-tools and pkgin among them). run0 comes last of the auto-detected three because it authorizes through polkit and needs one running, so a host with a working sudo or doas keeps it. pkexec is effectively reachable through --sudo-command alone: it needs a polkit rule granting org.freedesktop.policykit.exec, so it is no sound automatic choice, and it ships wherever polkit does, which is nearly every host that carries sudo anyway. Windows has two of its own, tried in turn: gsudo first, because one answered gsudo cache on covers every later escalation of a run, then Microsoft’s own sudo, inbox from Windows 11 24H2 and named win-sudo to --sudo-command, which caches nothing and so raises a UAC dialog per escalation.

A sudo on PATH is checked before it is preferred, because the name does not always belong to sudo. Alpine packages doas-sudo-shim, which installs a /usr/bin/sudo script forwarding to doas and accepting --non-interactive out of everything mpm sends: escalation through it works while every credential probe fails on an unknown option, which reads as a cold cache on a host that never asks for a password. So mpm runs sudo --version first and drives doas directly where that does not answer as sudo.

Name one explicitly with --sudo-command, or with its [mpm] sudo_command config key:

[mpm]
sudo_command = "doas"

This selects the binary only. Whether a manager escalates at all stays the separate decision of --sudo / --no-sudo and each manager’s own policy, described next.

Caution

A command escalated through run0 outlives mpm. run0 hands the payload to a systemd transient service, which runs outside mpm’s process tree, so a run that times out or is interrupted kills the run0 client and leaves the command running as root. Measured on Arch: an mpm --sudo-command run0 --pacman sync cut short at its timeout left run-p1084-i1084.service active minutes later, still holding /var/lib/pacman/db.lck and blocking every later pacman. systemctl list-units 'run-*' finds a survivor and systemctl stop retires it. Neither sudo nor doas behaves this way: their children sit in mpm’s own process group and go with it.

They are not interchangeable underneath. doas takes short options only, so mpm escalates through doas -n where it would write sudo --non-interactive. It also has no way to authenticate without running a command, so the credential probe runs doas -n true, and its persistence is opt-in per rule in doas.conf: mpm therefore never refreshes a doas credential on a schedule, where it keeps a sudo one alive for the whole run. A host with neither binary gets one warning, and its managers run unprivileged rather than failing on a missing sudo.

Escalator support

sudo, doas, run0, pkexec, gsudo and Microsoft’s own Windows sudo are the backends mpm drives today. What decides whether another one can join is not escalation itself, which they all do, but the credential probe: mpm reads the cache before a run so no password prompt lands inside the concurrent fan-out. A backend that cannot answer “am I already authorized?” without prompting cannot be primed, and every one of its calls risks a prompt nobody is watching.

That splits the candidates in two. sudo, sudo-rs and doas keep a timestamp mpm can read and refresh. run0 and pkexec broker authorization through polkit, where the grant is a property of the session and there is no cache to inspect, extend or keep warm. Windows is a third case again, where UAC is a consent dialog by design.

Escalator

Non-interactive

Probe, no prompt

Per-command query

Brands prompt

Refreshable

sudo

--non-interactive

--validate

--list

--prompt

sudo-rs

doas

-n

✅ runs true

run0

--no-ask-password

✅ runs true

pkexec

✅ via pkcheck

gsudo

status --json, read as output

❌ cache, not a timestamp

win-sudo

✅ via reg query, its mode only

❌ no cache at all

  • sudo-rs is the Rust rewrite Ubuntu ships as its default sudo since 25.10, so it arrives under the same name and needs no entry of its own. It answers every argv mpm sends. Two wordings differ and mpm now covers both: sudo --version prints sudo-rs <version> rather than Sudo version <version>, and a --validate denial quotes HAL 9000 where --list says may not run sudo.

  • run0 ships with systemd since 256 and fits mpm’s capture model: --pipe keeps stdout and stderr separate and propagates the exit code. It has no prompt control (systemd/systemd#33902) and no caller-side keepalive, polkit owning retention. Its refusal names neither run0 nor a wording mpm recognizes, and a setup failure can print nothing at all (systemd/systemd#35930), so the exit code has to be the primary signal. It resets HOME to /root and TERM to dumb, so anything a manager needs travels through --setenv. systemd 262 adds -n and --validate as sudo-compatible spellings (systemd/systemd#42465). It also inherits nothing from the caller, so mpm carries a forced environment across itself, splicing a --setenv= for each variable: without one, the four managers that force an environment would lose it, nala, tazpkg and urpmi pinning their parsers with LC_ALL=C and ports staying out of a dialog with BATCH=yes.

  • pkexec is reachable through --sudo-command pkexec, and auto-detection essentially never lands on it: it ships wherever polkit does, and those hosts carry sudo too. It only works where a polkit rule already grants org.freedesktop.policykit.exec, because it has no non-interactive flag and always executes a program. That is what pkcheck is for: polkit’s own query tool answers whether the action is authorized without running or prompting, so mpm probes with it and declines the run on an unauthorized host instead of stopping on a prompt inside the fan-out. Grant it with a rule returning polkit.Result.YES:

    // /etc/polkit-1/rules.d/49-mpm.rules
    polkit.addRule(function(action, subject) {
        if (
            action.id == "org.freedesktop.policykit.exec" &&
            subject.isInGroup("wheel")
        ) {
            return polkit.Result.YES;
        }
    });
    

    mpm passes --keep-cwd, since pkexec otherwise runs the command in the target user’s home. It strips the environment like run0, with the same consequence for the four managers that inject one, and it takes no -- separator: nothing needs one, because the first argument after it is already the manager’s absolute path.

  • gsudo and Windows sudo both gate on UAC, which is interactive by design; neither can fail instead of prompting (microsoft/sudo#7 asks for it on the Windows one, gerardog/gsudo#378 on the other). gsudo caches credentials, but the cache is scoped to the calling process by default, so warming it from one subprocess does not carry into the next. Its -n means new window, not non-interactive, and installing it through Scoop also shims a sudo name that can collide with the one Windows 11 24H2 ships. Windows sudo caches nothing and defaults to opening a new window, which breaks output capture, so mpm pins --inline and probes its mode instead of a cache: --inline errors against any other mode rather than falling back, and the mode is read from the registry rather than from sudo config, which words its answer in the display language. Driven on build 26100.1742 from a medium-integrity shell, the one path that really brokers an elevation, the command line reaches the child unchanged, --preserve-env carries a forced environment across and the exit code comes back, so microsoft/sudo#117 does not reach the argv mpm assembles. What it refuses outright is a token whose elevation type is Default, which is what a network logon carries: an SSH or scheduled run is turned away with You are not allowed to run sudo however privileged it already is, so unattended Windows escalation goes through gsudo or nowhere.

    Driving gsudo 2.6.1 settled what it can answer. It passes stdout and exit codes through faithfully, and gsudo status --json reports IsElevated, IntegrityLevel and CacheAvailable without ever prompting, which is the shape of a probe. What rules it out today is that status exits 0 whatever it finds, so the answer is only in its output, where probe_args reads an exit code alone. Failing to elevate is the reverse: it exits 999, seen as 231 once truncated to a byte, and says Error: Unable to connect to the elevated service., naming neither itself nor sudo.

Verified by driving each backend: run0 and pkexec on Arch Linux with systemd 261 and polkit 127, sudo-rs on Ubuntu 26.04, gsudo 2.6.1 on Windows 11 21H2, and Windows sudo 1.0.0 on Windows 11 24H2, build 26100.1742. Note what an SSH session on Windows cannot show, and it differs per backend: OpenSSH hands a local administrator a token that is already elevated but of type Default, which gsudo reads as needing no escalation and Windows sudo refuses outright. Either way the UAC path appears only in the filtered token an interactive desktop session carries, so reaching it means a medium-integrity shell, launched from the Start menu or through gsudo --integrity. please is a further candidate nobody has assessed yet, so it has no row rather than an empty one.

Controlling escalation

Override the default globally with --sudo / --no-sudo, or per manager with the sudo key of a [mpm.overrides.<id>] section:

[mpm]
sudo = false # Same as passing --no-sudo on every run.

[mpm.overrides.npm]
sudo = true # Run global npm installs through sudo.

[mpm.overrides.pacman]
sudo = false # Rootless setup: never escalate pacman.

The global flag has its own [mpm] sudo key, so a standing policy needs no flag on the command line.

A per-manager sudo value wins over the global flag, so you can escalate everything with --sudo while keeping a single manager rootless, or the reverse.

One prompt, up front

mpm runs managers concurrently with their output muted behind a progress bar, so a sudo password prompt raised mid-run is easy to miss and can stall the whole command. Before a state-changing command (install, upgrade, remove, sync, cleanup, restore) that involves escalation, mpm therefore probes the credential cache without prompting. A cache found warm (a prior sudo --validate, a NOPASSWD rule, a recent privileged command) is silently kept fresh for the rest of the run, and every escalated call spends it: no prompt at all. While that keepalive runs, the terminal holds live sudo credentials, so anyone at the keyboard can interrupt mpm and reuse them until they expire: the same exposure as any pre-authenticated sudo session, worth knowing before walking away from a long run.

The decision path, from the up-front probe to the end of the run:

        flowchart TD
    begin(["Mutating subcommand: install, upgrade,<br/>remove, sync, cleanup, restore"]) --> skip{"Windows, running as root,<br/>dry run or plan,<br/>or nothing escalates?"}
    skip -->|"yes"| bare["No sudo machinery:<br/>managers run as themselves"]
    skip -->|"no"| audit["Tamper audit: warn on an escalated<br/>binary that others can modify"]
    audit --> pick{"Which escalator does<br/>the host carry?"}
    pick -->|"none"| noesc["One warning:<br/>escalations run unprivileged"]
    pick -->|"sudo, or doas"| probe["Probe its credential cache,<br/>without prompting"]
    probe -->|"warm"| keepalive["Silent keepalive: every escalated call<br/>spends the cache, refreshed<br/>until the run ends"]
    probe -->|"cold: sudoers<br/>denies the user"| denied["One warning, no prompt:<br/>escalations fail fast"]
    probe -->|"cold:<br/>no terminal"| notty["One warning:<br/>escalations fail fast"]
    probe -->|"cold:<br/>on a terminal"| who{"Does mpm itself<br/>escalate a manager?"}
    who -->|"yes"| prompt["One branded password prompt<br/>for the whole run"]
    who -->|"no: internal<br/>escalators only"| stall["No prompt: the 30 s stall notice<br/>flags a hidden mid-run prompt"]
    prompt -->|"authenticated"| keepalive
    prompt -->|"refused"| failed["One warning:<br/>escalations may fail"]
    keepalive -->|"credentials dropped mid-run:<br/>every Homebrew command resets them"| dropped["One warning:<br/>stall notices re-arm"]
    dropped -->|"a new sudo authentication<br/>in the same terminal"| keepalive
    

Only a cold cache, on an interactive terminal, leads to a prompt: a notice names the managers about to escalate and the subcommand, then a single branded sudo prompt authenticates once for the whole run, so nothing blocks in the fan-out:

$ mpm upgrade
apt, deb-get need administrator rights to upgrade.
[mpm] password for kevin (running apt, deb-get):

The prompt names the account whose password sudo accepts, which is not always the caller. A targetpw, rootpw or runaspw policy asks for a different one, and openSUSE ships Defaults targetpw, so the same prompt reads password for root there.

Off a terminal (a pipe, CI, a desktop menu), mpm cannot prompt: a warning names the managers needing root, and they fail fast with a clear error instead of hanging. To escalate unattended, configure a NOPASSWD rule for the managers’ commands: mpm then asks sudo --list whether each escalated command is granted without a password, and proceeds silently when they all are. That second question is needed because sudo --validate answers a different one: it refuses whenever any matching sudoers entry wants a password, so a NOPASSWD rule reads as a cold cache as soon as a distribution’s stock ALL ALL=(ALL) ALL sits beside it, which is what openSUSE ships. A prior sudo --validate also works, but only from the same terminal session mpm runs in: under sudo’s default terminal-keyed timestamps, credentials cached in one terminal do not carry to a mpm launched without one (a desktop frontend, a CI step), so NOPASSWD is the robust choice there.

Write that rule through visudo, which validates the file before installing it: a syntax error saved straight into sudoers locks the account out of sudo entirely. visudo runs whatever EDITOR names, so an append needs no interactive editor:

$ echo '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' | sudo EDITOR='tee -a' visudo
%wheel ALL=(ALL:ALL) NOPASSWD: ALL

Append it to /etc/sudoers rather than dropping it into /etc/sudoers.d, or check afterwards that it took effect. Order decides which rule wins, and sudoers(5) states it plainly: “Where there are multiple matches, the last match is used (which is not necessarily the most specific match)”. An @includedir /etc/sudoers.d splices each drop-in in at the position of that directive, so a stock %wheel ALL=(ALL:ALL) ALL sitting after it silently restores the password prompt and the drop-in reads as if it were never written. sudo --list reports what actually applies, which is the question mpm itself asks.

One tool undoes the priming from inside the run: every Homebrew command resets the sudo timestamp at startup, on purpose, so a run mixing brew or cask with escalating managers can lose its credentials mid-flight. mpm warns when its background refresh finds them gone, and the managers escalating internally regain their hidden-prompt notice. A NOPASSWD rule is immune to the reset.

The probe also reads sudo’s answer: a user the sudoers policy does not authorize at all gets one warning and no password prompt, since a password could not change the answer.

Managers escalating internally

Some managers run sudo from inside their own commands: on macOS, brew escalates while installing a cask with a privileged payload (the macfuse example above) and fink re-execs its root commands through sudo, while on Linux the AUR helpers call sudo pacman for their privileged phases, pacstall re-execs itself through sudo pacstall, shelly calls sudo from inside every mutating verb, and topgrade drives each privileged step through its own per-step sudo. mpm never wraps these managers in sudo (brew even refuses to run as root, and topgrade warns and prompts when launched as root), and most of their runs never escalate, so a stock mpm upgrade does not pre-authenticate for them: prompting on every run would be worse than the rare mid-run prompt it avoids.

Three mechanisms cover that rare prompt instead. When the up-front probe finds the credential cache already warm, the keepalive is armed for internal escalators too, so their mid-run sudo spends the cache silently. On a cold cache, such a manager is held back from the concurrent batch and run last, on its own (why), and its call runs without a spinner. The prompt the tool prints therefore stays on a still terminal, to be answered. A call that then stays silent for 30 seconds draws a warning:

$ mpm install macfuse
(...)
warning:cask: No output for 30s: may be waiting on a hidden password prompt. Last output: "==> Running installer for macfuse; your password may be necessary."

For a guaranteed one-prompt experience, opt the manager into up-front authentication with a scoped sudo = true override:

[mpm.overrides.cask]
sudo = true # Authenticate up front before any privileged cask payload.

or scope the global flag to the manager: mpm --cask --sudo upgrade. Prefer these to a bare mpm --sudo upgrade, which is broader than it looks: the global flag covers every selected manager, and also activates dormant privileged markers like those of pip, npm, gem and cpan, wrapping their system-scope installs in sudo. Left dormant, those markers still pay off on failure: an operation carrying one that dies on a permission error (a root-owned npm prefix, a system Ruby) draws a warning naming this scoped opt-in, right after the tool’s own account of the refusal.

Running mpm itself as root

On Linux you may instead install and run mpm under sudo, so every manager it drives is already privileged:

$ sudo uv tool install meta-package-manager
(...)
$ sudo mpm upgrade
(...)

Security

Escalating a manager runs its binary, and every install script of the packages it touches, as root. mpm only escalates the managers that require it or that you have opted in, and it warns when a sudo override is read from an untrusted config source. See the security model for the trust rules behind this.