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:
objectOne 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:
doasis not asudoclone with another name, it takes short options only and has no way to authenticate without running a command.- 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 byCLIExecutor.build_cliand matched back byCLIExecutor.runto recognize an escalation failure, so the two sites must stay in lockstep.
- probe_args: tuple[str, ...]¶
Argv reading the credential cache without ever prompting.
sudoanswers this without running anything (--validate);doashas no such mode, so it runstrueas 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-interactivewhen a password would be wanted.doashas no such query and getsNone.Needed because
probe_argsanswers 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 taggedNOPASSWD. openSUSE stacks exactly that pair: its stockALL ALL=(ALL) ALLsits under theNOPASSWDruledocs/sudo.mdrecommends, so--validatereports 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
--validaterather than of one distribution’s policy, and Fedora reaches it by an unrelated route: its stock%wheel ALL=(ALL) ALLunder the sameNOPASSWDrule leavessudo 1.9.17p2answering a password is required``and exiting``1`, while``–list –exits``0` and a real ``sudo --non-interactive id -ureturns0. Two policies with nothing in common, one false cold cache, which is what this second probe exists to catch.
- refreshable: bool¶
Whether
probe_argscan also serve as a keepalive tick.True only where the escalator caches credentials on a schedule mpm can reason about.
doaspersistence is opt-in per rule indoas.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 --promptcan;doascannot. The noticeprime_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 fromid.Only Microsoft’s
sudo.exeneeds one. It ships under a name the Unix escalator already claims, and shares none of its dialect:--non-interactiveand--validateare not options it has. So the two need distinct ids, for thesudo_commandoverride to name either, whileresolve_escalator()still has to look for the same file name on both platforms.
- identity_args: tuple[str, ...] | None = None¶
Argv proving the binary on
PATHis this escalator, not a stand-in.A name on
PATHis not proof of the dialect behind it: Alpine’sdoas-sudo-shiminstalls/usr/bin/sudoas a shell script forwarding todoas, and it accepts--non-interactivealone out of everything mpm sends. Soescalate_argsworks there while every probe dies onunrecognized 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 --versionreports the build and exits0on real sudo, where the shim rejects the option and exits1.Nonewhere no stand-in is known, which is every escalator butsudo.
- identity_markers: tuple[str, ...] | None = None¶
Substrings
identity_argsprints 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 --versionprintsSudo version 1.9.17on the original andsudo-rs 0.2.13on the Rust rewrite Ubuntu ships as its defaultsudosince25.10. Matching the first alone rejected sudo-rs as a stand-in, andresolve_escalator()then fell through todoason a host carrying both, inverting the documented preference. Upstream’sSUDO_RS_VERSIONoverride replaces the number and never thesudo-rsprefix, 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 versionto 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_argsfound escalation ready, for an escalator whose probe cannot say so through its exit code. Any one is enough.Nonekeeps the exit code as the whole answer, which is whatsudo,doasandrun0report through.gsudois the exception:gsudo status --jsonasks exactly the questionprime_sudo()needs and always exits0, so the answer lives in its output alone. Two of its fields settle it, and either is sufficient:IsElevatedbecause a process already elevated needs no escalation, andCacheAvailablebecause 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
Nonefor an escalator the variables survive on their own.Only
run0needs it. It runs the command in a transient service forked from the service manager, which inherits nothing from the caller, so the environmentCLIExecutor.runforces on a call would reachrun0and stop there. That silently costs the managers relying on one:nala,tazpkgandurpmipin their parsers against a translated locale withLC_ALL=C, andportskeeps out of an interactive dialog withBATCH=yes.sudoanddoasneed no template: both reset the environment too, but through asudoerspolicy whoseenv_keepthe host owns, which is where such a decision belongs.pkexecclears it outright and would need thepkexec env NAME=VALUE ...idiom rather than a per-variable flag, so it keepsNoneuntil a manager on a polkit-only host asks for one.
- property binary_name: str¶
The file
resolve_escalator()looks for onPATH.
- forward_env(args, env)[source]¶
Splice
env_forward_templatefor each variable intoargs.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
argsuntouched for an escalator declaring no template, or for a call forcing no variable.A
Nonevalue asks for a variable to be unset, which needs carrying nowhere: the fresh environment on the far side of the escalation never had it.
- probe_says_warm(probe)[source]¶
Whether the probe found escalation ready to run unprompted.
- Return type:
- resolved_probe_args()[source]¶
probe_args, with any:pid:token replaced by mpm’s own id.Only
pkexecneeds it. Its probe ispkcheck, polkit’s own authorization query, which asks about a subject rather than about the caller and refuses to guess one: without--processit exits126onSubject not specified. Every other escalator answers for whoever runs it and carries no token, so the substitution is a no-op there.
- is_genuine()[source]¶
Whether the binary on
PATHreally is this escalator.Truewhen the escalator declares noidentity_args, so an escalator opts into the check rather than out of it.- Return type:
- 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.
sudocomes first so a host carrying both keeps the behavior it has today, and thesudo_commandoverride exists for the user who wants the other one. The order only decides auto-detection: an explicit override always wins.run0comes after them for the same reason, one step further: it needs a running polkit to authorize anything, so a host carrying a workingsudoordoaskeeps it, and run0 answers for the systemd hosts that ship neither.win-sudocloses the list, behindgsudo, and the two are the whole of Windows. Microsoft’s is inbox from24H2wheregsudohas 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 answeredgsudo cache oncovers every later one. It is the fallback for a host carrying nogsudo, not the default for a host carrying both.pkexecsits before them, and auto-detection essentially never reaches it: it ships wherever polkit does, which is nearly every desktop Linux, and those carrysudotoo. It is there for--sudo-command pkexec, and it only works where a polkit rule already grantsorg.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
Nonewhen the host carries none.With no
override, returns the first entry ofESCALATORSwhose binary is onPATHand passesis_genuine(), so a host withoutsudostill escalates through whatever it does have, and one whosesudois a stand-in for another escalator drives that other one directly. Anoverridenames one by itsidand 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
PATHdoes not move mid-run and the identity probe costs a subprocess. Tests changing what is installed must callresolve_escalator.cache_clear().
- 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_clineeds 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:
objectOwnership snapshot of the tree a manager’s global installs write into.
Built by
inspect_install_root()from a manager’s owninstall_rootprobe. 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 scopedsudo = trueoverride stays the one road to escalating a dormant marker, and the failure-gate hint (_is_permission_failure()) is what names it.
- meta_package_manager.sudo.inspect_install_root(manager)[source]¶
Resolve and stat
manager’s install root, orNonewhen unknowable.Nonecovers every dead end: a non-POSIX host (the ownership model does not apply, andpwddoes 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:
- meta_package_manager.sudo.prime_sudo(ctx, managers)[source]¶
Warm the
sudocredential 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-authenticatedsudo --validate, aNOPASSWDrule, 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 ownsudo --non-interactiveas well as a manager’s internalsudo(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 brandedsudopassword 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 tosudo, runs their code as root, so it draws one warning per manager (seedocs/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
sudoexecutable 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
sudoat all (_is_sudo_denied()): one warning names the managers mpm escalates and the remedy, since a prompt could only collect a passwordsudothen 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: