meta_package_manager.sudo module¶

Privilege-escalation machinery for the mutating fan-outs.

This module owns sudo credential priming (prime_sudo()) and its background keepalive (_start_sudo_keepalive()), escalation-policy resolution (_resolved_sudo()), sudo-failure detection (_is_sudo_auth_failure()), and the hidden-prompt stall watchdog (_StallWatchdog). The execution engine (meta_package_manager.execution) consumes the policy pieces to wrap and diagnose escalated commands; the CLI calls prime_sudo() at the top of each mutating subcommand.

Why priming exists: a concurrent state-changing command mutes per-manager output and feeds each child stdin=/dev/null, so a sudo password prompt raised mid-run (by mpm’s own sudo --non-interactive or by a manager that escalates internally, like Homebrew cask) lands invisibly on /dev/tty and can stall the run up to the mutating timeout. Priming first probes the credential cache non-interactively: found warm, it is silently kept alive for the whole run; found cold on a terminal, the managers mpm itself escalates get a single up-front password prompt, naming them and branded [mpm]. Internal escalators never prompt up front: their rare cold-cache escalation is covered by the silent-call stall notice instead, raised while the hidden prompt can still be answered.

Note

Windows reaches only a corner of this module. gsudo gives it an escalator to probe and a cache to warm, and Microsoft’s own sudo.exe backs it up from 24H2, but no manager there escalates by default, so prime_sudo() returns on the empty selection unless --sudo or a [mpm.overrides.<id>] sudo = true entry asks for it. The internal escalators stay macOS-only managers, so the watchdog is not armed there either.

Neither reaches an unattended run the way a Unix escalator does. sudo.exe refuses every token whose elevation type is Default, which is what a network logon carries: an SSH session holding a full administrator token at High integrity is still turned away with You are not allowed to run sudo. So a scripted or remote Windows run escalates through gsudo or not at all.

Todo

Prime Microsoft’s sudo.exe once there is anything to prime. It caches nothing, so every escalation of a run raises a UAC dialog of its own, which is why ESCALATORS ranks it behind gsudo rather than beside it. microsoft/sudo#7 is the request that would give it a cache for prime_sudo() to warm; gsudo has the same gap open at gerardog/gsudo#378 for its own password path. Nothing else blocks the backend: the command line, the --preserve-env environment and the child’s exit code all survive the elevation, measured on build 26100.1742 from a Medium-integrity shell, the one path that really brokers it, so microsoft/sudo#117 never reaches the shapes this project builds.

Emulate an option a backend cannot express rather than failing on it: topgrade returns a hard error there, which its users report as a bug (topgrade-rs/topgrade#1435).

Todo

Escalate to the user owning a manager’s tree, not only to root: every escalate_args reaches root alone, where sudo --user and doas -u could reach the owner. The one legitimate case is a multi-user nix install, whose foreign-owned profiles are a first-class upstream configuration. A shared Homebrew prefix is not: Homebrew’s support tiers file “Multi-user Homebrew environments where multiple users share the same installation” as unsupported, so smoothing that setup over (as topgrade does for its brew step) would carry a burden upstream itself refuses. Stays unbuilt until a nix user asks.

Todo

Rebrand the hidden password prompt of an internal escalator with a SUDO_ASKPASS helper, once the stall notice of _StallWatchdog proves insufficient in the field. It is also the only route serving a hardened sudoers policy, whose timestamps the primed cache cannot reach (see _SUDO_CACHE_WARM). That class records why the helper was rejected first, and any implementation has to answer its two remaining points: the raw password it handles, and the tools it never reaches (brew honors the variable, fink’s plain sudo re-exec does not). The third, a still terminal for the prompt, _hidden_prompt_risk() now provides.

class meta_package_manager.sudo.Escalator(id, escalate_args, probe_args, passwordless_probe_args, prompt_args, refreshable, brands_prompt, binary=None, identity_args=None, identity_markers=None, probe_success_markers=None, env_forward_template=None)[source]¶

Bases: object

One privilege-escalation binary, and the argv dialects mpm drives it with.

Everything specific to an escalator lives here, so the rest of the module reasons about escalation without naming a binary. The dialects are not interchangeable: doas is not a sudo clone with another name, it takes short options only and has no way to authenticate without running a command.

id: str¶

The binary’s name, and the value the sudo_command override selects it by.

escalate_args: tuple[str, ...]¶

Argv prefix escalating a manager command, non-interactively.

Non-interactive on purpose: a prompt raised inside the concurrent fan-out lands on a terminal nobody is watching (see prime_sudo()). Emitted by CLIExecutor.build_cli and matched back by CLIExecutor.run to recognize an escalation failure, so the two sites must stay in lockstep.

probe_args: tuple[str, ...]¶

Argv reading the credential cache without ever prompting.

sudo answers this without running anything (--validate); doas has no such mode, so it runs true as the cheapest harmless command.

passwordless_probe_args: tuple[str, ...] | None¶

Argv asking whether one named command runs without a password, or None.

Completed with the command’s path by _escalation_is_passwordless(). sudo --list -- <command> answers precisely, reporting the command when the policy grants it unauthenticated and failing under --non-interactive when a password would be wanted. doas has no such query and gets None.

Needed because probe_args answers a different question. sudo –validate``refuses whenever any matching``sudoers entry requires a password, even when the entry that would actually run the command is tagged NOPASSWD. openSUSE stacks exactly that pair: its stock ALL ALL=(ALL) ALL sits under the NOPASSWD rule docs/sudo.md recommends, so --validate reports a cold cache on a host where every escalation in fact runs untouched, and mpm warned that managers “may fail” before they went on to succeed.

That is a property of --validate rather than of one distribution’s policy, and Fedora reaches it by an unrelated route: its stock %wheel ALL=(ALL) ALL under the same NOPASSWD rule leaves sudo 1.9.17p2 answering a password is required``and exiting``1`, while``–list –exits``0` and a real ``sudo --non-interactive id -u returns 0. Two policies with nothing in common, one false cold cache, which is what this second probe exists to catch.

prompt_args: tuple[str, ...]¶

Argv authenticating the user up front, interactively, once per run.

refreshable: bool¶

Whether probe_args can also serve as a keepalive tick.

True only where the escalator caches credentials on a schedule mpm can reason about. doas persistence is opt-in per rule in doas.conf, so a recurring probe would report a drop on every tick for the majority of hosts, where no rule asks for it. Those escalators get no keepalive thread at all.

brands_prompt: bool¶

Whether the escalator can be told what password prompt to print.

sudo --prompt can; doas cannot. The notice prime_sudo() echoes before authenticating names the managers either way, so an unbranded prompt loses the [mpm] marker, not the explanation.

binary: str | None = None¶

The name to look for on PATH, where it differs from id.

Only Microsoft’s sudo.exe needs one. It ships under a name the Unix escalator already claims, and shares none of its dialect: --non-interactive and --validate are not options it has. So the two need distinct ids, for the sudo_command override to name either, while resolve_escalator() still has to look for the same file name on both platforms.

identity_args: tuple[str, ...] | None = None¶

Argv proving the binary on PATH is this escalator, not a stand-in.

A name on PATH is not proof of the dialect behind it: Alpine’s doas-sudo-shim installs /usr/bin/sudo as a shell script forwarding to doas, and it accepts --non-interactive alone out of everything mpm sends. So escalate_args works there while every probe dies on unrecognized option, and mpm reads that as a cold credential cache on a host where escalation in fact runs untouched.

Run by is_genuine(), which needs the argv to authenticate nothing and to cost nothing: sudo --version reports the build and exits 0 on real sudo, where the shim rejects the option and exits 1. None where no stand-in is known, which is every escalator but sudo.

identity_markers: tuple[str, ...] | None = None¶

Substrings identity_args prints when the binary is genuine, any one of which is proof.

Matched against <stdout> on a zero exit. Kept beside the argv because exit status alone is too weak a signal: a stand-in free to accept the option would pass on the returncode.

Several markers rather than one because a reimplementation brands its own banner: sudo --version prints Sudo version 1.9.17 on the original and sudo-rs 0.2.13 on the Rust rewrite Ubuntu ships as its default sudo since 25.10. Matching the first alone rejected sudo-rs as a stand-in, and resolve_escalator() then fell through to doas on a host carrying both, inverting the documented preference. Upstream’s SUDO_RS_VERSION override replaces the number and never the sudo-rs prefix, so the prefix is what the second marker keys on.

Both markers are the ones sudo-rs itself sorts the two implementations by: its test framework reads the same banner, stripping Sudo version to recognize the original and treating everything else as its own (test-framework/sudo-test/src/lib.rs).

probe_success_markers: tuple[str, ...] | None = None¶

Substrings proving probe_args found escalation ready, for an escalator whose probe cannot say so through its exit code. Any one is enough.

None keeps the exit code as the whole answer, which is what sudo, doas and run0 report through. gsudo is the exception: gsudo status --json asks exactly the question prime_sudo() needs and always exits 0, so the answer lives in its output alone. Two of its fields settle it, and either is sufficient: IsElevated because a process already elevated needs no escalation, and CacheAvailable because a warm credentials cache spends without raising a UAC dialog.

env_forward_template: str | None = None¶

Argument forwarding one environment variable across the escalation, or None for an escalator the variables survive on their own.

Only run0 needs it. It runs the command in a transient service forked from the service manager, which inherits nothing from the caller, so the environment CLIExecutor.run forces on a call would reach run0 and stop there. That silently costs the managers relying on one: nala, tazpkg and urpmi pin their parsers against a translated locale with LC_ALL=C, and ports keeps out of an interactive dialog with BATCH=yes.

sudo and doas need no template: both reset the environment too, but through a sudoers policy whose env_keep the host owns, which is where such a decision belongs. pkexec clears it outright and would need the pkexec env NAME=VALUE ... idiom rather than a per-variable flag, so it keeps None until a manager on a polkit-only host asks for one.

property binary_name: str¶

The file resolve_escalator() looks for on PATH.

forward_env(args, env)[source]¶

Splice env_forward_template for each variable into args.

Inserted ahead of the end-of-options separator the prefix closes on, so a manager’s own flags stay shielded from the escalator’s parser. Sorted for a stable command disclosure. Returns args untouched for an escalator declaring no template, or for a call forcing no variable.

A None value asks for a variable to be unset, which needs carrying nowhere: the fresh environment on the far side of the escalation never had it.

Return type:

tuple[str, ...]

probe_says_warm(probe)[source]¶

Whether the probe found escalation ready to run unprompted.

Return type:

bool

resolved_probe_args()[source]¶

probe_args, with any :pid: token replaced by mpm’s own id.

Only pkexec needs it. Its probe is pkcheck, polkit’s own authorization query, which asks about a subject rather than about the caller and refuses to guess one: without --process it exits 126 on Subject not specified. Every other escalator answers for whoever runs it and carries no token, so the substitution is a no-op there.

Return type:

tuple[str, ...]

is_genuine()[source]¶

Whether the binary on PATH really is this escalator.

True when the escalator declares no identity_args, so an escalator opts into the check rather than out of it.

Return type:

bool

meta_package_manager.sudo.ESCALATORS: Final[tuple[Escalator, ...]] = (Escalator(id='sudo', escalate_args=('sudo', '--non-interactive'), probe_args=('sudo', '--non-interactive', '--validate'), passwordless_probe_args=('sudo', '--non-interactive', '--list', '--'), prompt_args=('sudo', '--validate'), refreshable=True, brands_prompt=True, binary=None, identity_args=('sudo', '--version'), identity_markers=('Sudo version', 'sudo-rs'), probe_success_markers=None, env_forward_template=None), Escalator(id='doas', escalate_args=('doas', '-n'), probe_args=('doas', '-n', 'true'), passwordless_probe_args=None, prompt_args=('doas', 'true'), refreshable=False, brands_prompt=False, binary=None, identity_args=None, identity_markers=None, probe_success_markers=None, env_forward_template=None), Escalator(id='run0', escalate_args=('run0', '--pipe', '--no-ask-password', '--'), probe_args=('run0', '--pipe', '--no-ask-password', 'true'), passwordless_probe_args=None, prompt_args=('run0', '--pipe', 'true'), refreshable=False, brands_prompt=False, binary=None, identity_args=('run0', '--version'), identity_markers=('systemd',), probe_success_markers=None, env_forward_template='--setenv={name}={value}'), Escalator(id='pkexec', escalate_args=('pkexec', '--keep-cwd'), probe_args=('pkcheck', '--action-id', 'org.freedesktop.policykit.exec', '--process', '{pid}'), passwordless_probe_args=None, prompt_args=('pkexec', '--keep-cwd', 'true'), refreshable=False, brands_prompt=False, binary=None, identity_args=None, identity_markers=None, probe_success_markers=None, env_forward_template=None), Escalator(id='gsudo', escalate_args=('gsudo',), probe_args=('gsudo', 'status', '--json'), passwordless_probe_args=None, prompt_args=('gsudo', 'cache', 'on', '--pid', '0'), refreshable=False, brands_prompt=False, binary=None, identity_args=('gsudo', '--version'), identity_markers=('gsudo',), probe_success_markers=('"IsElevated":true', '"CacheAvailable":true'), env_forward_template=None), Escalator(id='win-sudo', escalate_args=('sudo', '--inline', '--preserve-env'), probe_args=('reg', 'query', 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Sudo', '/v', 'Enabled'), passwordless_probe_args=None, prompt_args=('sudo', 'config'), refreshable=False, brands_prompt=False, binary='sudo', identity_args=('sudo', '--help'), identity_markers=('Sudo for Windows',), probe_success_markers=('0x3',), env_forward_template=None))¶

Every escalator mpm can drive, in the order it prefers them.

sudo comes first so a host carrying both keeps the behavior it has today, and the sudo_command override exists for the user who wants the other one. The order only decides auto-detection: an explicit override always wins.

run0 comes after them for the same reason, one step further: it needs a running polkit to authorize anything, so a host carrying a working sudo or doas keeps it, and run0 answers for the systemd hosts that ship neither.

win-sudo closes the list, behind gsudo, and the two are the whole of Windows. Microsoft’s is inbox from 24H2 where gsudo has to be installed, which is exactly why it ranks second: it caches nothing, so each escalation of a run raises its own UAC dialog, where one answered gsudo cache on covers every later one. It is the fallback for a host carrying no gsudo, not the default for a host carrying both.

pkexec sits before them, and auto-detection essentially never reaches it: it ships wherever polkit does, which is nearly every desktop Linux, and those carry sudo too. It is there for --sudo-command pkexec, and it only works where a polkit rule already grants org.freedesktop.policykit.exec, since it cannot escalate without prompting. The probe is what keeps that honest: a host without the rule reports a cold cache and its managers decline to run, rather than each of them stopping on a prompt inside the fan-out.

meta_package_manager.sudo.resolve_escalator(override: str | None = None) Escalator | None[source]¶

The escalator mpm drives, or None when the host carries none.

With no override, returns the first entry of ESCALATORS whose binary is on PATH and passes is_genuine(), so a host without sudo still escalates through whatever it does have, and one whose sudo is a stand-in for another escalator drives that other one directly. An override names one by its id and is honored even when the binary is missing, so the failure names the escalator the user asked for instead of silently falling back to another one.

Cached on the override, since PATH does not move mid-run and the identity probe costs a subprocess. Tests changing what is installed must call resolve_escalator.cache_clear().

Return type:

Escalator | None

meta_package_manager.sudo.ESCALATION: Final = <meta_package_manager.sudo._EscalationChoice object>¶

Process-wide escalator selection.

A module-level singleton for the same reason as PLAN_RECORDER: CLIExecutor.build_cli needs it from the fan-out’s worker threads, where the click context is not reliably reachable.

class meta_package_manager.sudo.InstallRoot(path, owner_uid, owner_name)[source]¶

Bases: object

Ownership snapshot of the tree a manager’s global installs write into.

Built by inspect_install_root() from a manager’s own install_root probe. Diagnosis only, by decision: deciding escalation from this snapshot was assessed and rejected, since silently running a manager as root on filesystem evidence nobody reviewed is a posture change no diagnostic payoff justifies. The scoped sudo = true override stays the one road to escalating a dormant marker, and the failure-gate hint (_is_permission_failure()) is what names it.

path: Path¶

The install root itself.

owner_uid: int¶

Numeric owner of the root directory.

owner_name: str¶

The owner’s account name, or the bare uid when no account matches.

meta_package_manager.sudo.inspect_install_root(manager)[source]¶

Resolve and stat manager’s install root, or None when unknowable.

None covers every dead end: a non-POSIX host (the ownership model does not apply, and pwd does not exist), a manager with no discovery verb, a probe that fails, and a resolved path that does not exist. The probe shells out, so failures of any kind are swallowed: this is diagnosis, and it must never break the command it decorates.

Return type:

InstallRoot | None

meta_package_manager.sudo.prime_sudo(ctx, managers)[source]¶

Warm the sudo credential cache, up front, for a mutating fan-out.

Probes the cache non-interactively (sudo --non-interactive --validate) before considering any prompt. A warm cache (pre-authenticated sudo --validate, a NOPASSWD rule, a recent run) is silently kept fresh for the whole invocation by _start_sudo_keepalive(), so every later escalation on the same terminal, mpm’s own sudo --non-interactive as well as a manager’s internal sudo (CLIExecutor.internal_sudo), spends the cache instead of blocking on an invisible prompt inside the concurrent fan-out. Only a cold cache, on an interactive terminal, with managers that mpm itself escalates (_resolved_sudo()), triggers the interactive path: a notice naming the managers and the subcommand, then a single branded sudo password prompt.

Before probing, the binary of each manager mpm escalates is audited with the same tamper test the config loader applies (config_file_is_trusted()): a binary that others can modify, handed to sudo, runs their code as root, so it draws one warning per manager (see docs/security.md).

Call at the top of each mutating subcommand, before the fan-out draws its spinner. Never prompts when:

  • Windows (no sudo) or the process is already root,

  • no selected manager escalates, through mpm or internally,

  • a dry run or a plan run (no state-changing CLI is executed),

  • already primed once this invocation (idempotent),

  • the sudo executable is missing (one warning is logged),

  • the probe finds the cache already warm (keepalive only, fully silent),

  • the probe reports the user is not authorized to run sudo at all (_is_sudo_denied()): one warning names the managers mpm escalates and the remedy, since a prompt could only collect a password sudo then rejects, while an internal-only selection stays silent,

  • no interactive terminal is available: one warning names the managers mpm escalates and leaves them to fail fast rather than block on a prompt no one can answer, while an internal-only selection stays silent, or

  • only internal escalators are selected on a cold cache: most such runs never escalate, so the rare mid-run prompt is covered by the silent-call stall notice instead.

Return type:

None