Skip to content

nvme: drop root privileges through the privsep helper - #4182

Merged
igaw merged 7 commits into
linux-nvme:masterfrom
dwsuse:privsep-cli
Oct 20, 2026
Merged

igaw merged 7 commits into
linux-nvme:masterfrom
dwsuse:privsep-cli

Conversation

@dwsuse

@dwsuse dwsuse commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Fourth step of privilege separation (#3879): nvme-cli uses the helper. When root runs nvme-cli on behalf of a user and the command does not need root, nvme-cli starts the helper, drops its privileges and opens the devices through the helper. Rebased onto master with #4185.

Still behind the privsep meson option (disabled by default, Linux only). The code is the same with the option disabled, except that it never starts a helper. setuid is not supported: a setuid nvme-cli keeps running as it was started and never trusts the environment.

Fix found by the privsep test runs: the callers of libnvme_get_nsid(), libnvme_reset_subsystem() and libnvme_rescan_ns() checked errno instead of the return value. A privsep handle leaves errno alone, so get-feature, set-feature and fdp set-events failed on a controller device, and ns get-id and format could report success after a failure.

Prep:

  • privsep handles report libnvme_transport_handle_is_direct(), so nvme format still rescans the namespaces, updates the block size and resets the controller
  • libnvme_set_ioctl_probing() reaches the helper: OPEN carries it in the former reserved word, the helper opens the handle in a context without probing and rejects unknown options
  • an nvme-cli.conf which can't be read is reported with its name and error instead of failing silently. The file holds no secrets and must be readable by every user, as it is read after the drop; nvme-cli.conf(5) says so

Which commands drop: an explicit needs_root flag in struct command marks the commands which keep root: the commands without a device argument (fabrics, enumeration, keys, exclusions, configuration, registry, NBFT, and the deprecated key commands) and the device commands which also access PCI config space, the kernel log or root owned paths (Micron vs-pcie-stats, clear-pcie-correctable-errors, vs-internal-log, WDC and Sandisk reason-identifier, vs-telemetry-controller-option, registry update and delete). The list is what has to be addressed before all commands can drop. Commands with an MI device (mctp:<net>,<eid> in their arguments) keep root, as MI uses the MCTP sockets directly.

When: first in main(), on the command plugin_lookup() found (#4185), before the configuration file or the options of the command are parsed. A needs_root command does not run once the privileges are dropped, should a deprecated command forward to one.

To whom: SUDO_UID/SUDO_GID, else NVME_PRIVSEP_UID/NVME_PRIVSEP_GID, only when nvme-cli runs as root. Without them root keeps its privileges, so scripts and services are unchanged. The supplementary groups become the target group only (no NSS as root). If the helper cannot be started or the privileges cannot be dropped, nvme-cli fails instead of running privileged.

For the tests, NVME_PRIVSEP_TEST_HELPER names a helper which an unprivileged nvme-cli uses as well; it is ignored as root and in secure-execution mode.

Testing:

  • libnvme tests: privsep handles are direct; a context without ioctl probing uses the 32 bit passthru in the helper; unknown OPEN options are rejected (each fails without the change)
  • unit test: an nvme-cli.conf which can't be read is reported (fails without the change)
  • CLI test (nvme-cli - privsep, needs the id plugin): device commands start the helper and the helper opens the device, commands which keep root, MI devices, help and version don't, a mctp: file name does not keep root; skipped when run as root
  • the mock CLI tests (print, pif-sts, init-pi-tags, dry-run, sel, host-metadata, discovery-logs, sed) also run with the commands going through the helper (nvme-cli - <name>-privsep)
  • full test suite with and without the option (gcc and clang, --werror), every commit on its own
  • real root on a VM against an nvme-loop target: plain root, NVME_PRIVSEP_UID, SUDO_UID, missing helper, needs_root commands, MI, setuid nvme keeps root and ignores the environment, nvme-cli.conf read after the drop with the command line winning
  • FreeBSD and Windows (x86_64, aarch64) builds

@igaw
igaw requested a balanced review from Copilot October 15, 2026 07:24
@igaw

igaw commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

I am not totally happy with this approach but given that we retrofitting privilege separation, it's no surprise that there are exception necessary at this point. Ideally we could drop the root rights right at the beginning without parsing anything. But this is not possible right now. So I think it's a step towards this goal.

Thus let's get the device commands working and figure out how to handle the fabrics related one in a different step.

@martin-belanger do you agree with this approach do you think this is a good idea?

I am still not sure if the whole privsep is going to work as hoped. I'd say we mark this as experiment and if it turns out shitty, we can remove it again.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Helper integration introduces functional regressions and an argument-based path that unintentionally retains root privileges.

5 open findings
What changed in this PR

Connects nvme-cli to the existing privilege-separation helper, allowing eligible commands to drop root privileges before parsing configuration and options. This remains Linux-only and disabled by default.

Changes:

  • Adds early command selection, privilege dropping and helper-backed device access.
  • Marks commands that must retain root privileges.
  • Adds command-selection checks and helper-backed CLI tests.
File Description
tests/​unit/​c/​test-plugin.c Tests command-selection consistency.
tests/​unit/​c/​meson.build Links privilege-separation support into tests.
tests/​cli/​nvme_privsep_test.py Tests helper startup and device routing.
tests/​cli/​meson.build Registers helper-backed CLI tests.
src/​privsep.h Declares privilege-separation interfaces and stubs.
src/​privsep.c Implements helper startup and privilege dropping.
src/​plugin.h Adds command privilege metadata and lookup interface.
src/​plugin.c Adds early lookup and execution checks.
src/​nvme.c Initializes privilege separation before configuration loading.
src/​nvme-cmds-fabrics.c Attaches contexts and preserves fabrics privileges.
src/​nvme-cmds-enumerate.c Preserves enumeration privileges.
src/​nvme-cmds-deprecated.c Preserves deprecated key-command privileges.
src/​meson.build Conditionally builds privilege-separation support.
src/​global-ctx.c Attaches shared contexts to the helper.
plugins/​zns/​zns.c Preserves listing privileges.
plugins/​wdc/​wdc-nvme.c Marks privileged telemetry commands.
plugins/​utils/​utils.c Exempts metadata dumping from privilege dropping.
plugins/​solidigm/​solidigm-nvme.c Exempts version reporting.
plugins/​seagate/​seagate-nvme.c Exempts version reporting.
plugins/​sandisk/​sandisk-nvme.c Marks privileged telemetry commands.
plugins/​registry/​registry-nvme.c Preserves registry-list privileges.
plugins/​netapp/​netapp-nvme.c Preserves enumeration privileges.
plugins/​nbft/​nbft-plugin.c Preserves NBFT-reading privileges.
plugins/​micron/​micron-nvme.c Marks privileged diagnostics and version commands.
plugins/​keys/​keys-plugin.c Preserves key-management privileges.
plugins/​huawei/​huawei-nvme.c Preserves listing privileges.
plugins/​exclusion/​exclusion-nvme.c Preserves exclusion-management privileges.
plugins/​config/​config-nvme.c Preserves configuration-management privileges.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/global-ctx.c Resolved
Comment thread src/privsep.c Resolved
Comment thread plugins/registry/registry-nvme.c Resolved
Comment thread src/privsep.c Resolved
Comment thread tests/cli/nvme_privsep_test.py Resolved
@martin-belanger

Copy link
Copy Markdown
Collaborator

Thus let's get the device commands working and figure out how to handle the fabrics related one in a different step.

@martin-belanger do you agree with this approach do you think this is a good idea?

I agree with this approach. Getting the device commands working first makes sense, and so does marking it as an experiment.

For the fabrics commands, here is an idea for the later step. nvme connect, connect-all, disconnect and discover could send the request to nvme-discoverd over Varlink, like systemctl does with systemd. nvme-cli would then drop root at the start for these commands too, and nvme-discoverd does the work as root. It already does the connects, writes the registry, uses the keyring and reads the DH-HMAC-CHAP secrets. It would also be the only process that writes the registry. nvme-discoverd checks who the caller is with SO_PEERCRED (or polkit).

Limits:

  • Root keeps the direct path, as in this PR. This keeps scripts, the initramfs and nvme-discoverd's own nvme connect units working without a daemon.
  • nvme-discoverd gets one more source of input to parse as root, so it may need its own privilege separation later.
  • The Varlink interface becomes an API we have to keep stable.

This is only a thought,

@dwsuse

dwsuse commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

That is also an option, that the fabrics command are just forwarding the request to discocverd. This might be an option for a production system. I think we still want the feature to be able to write directly to the fabrics device for development work. FWIW, the current version of the privsep helper is able to handle the fabrics command (writing to the fabrics device). What is missing are all the registry features.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/nvme.c Resolved
Comment thread tests/cli/meson.build Resolved
libnvme_get_nsid(), libnvme_reset_subsystem() and libnvme_rescan_ns()
return a negative error code. Only for a direct handle errno happens to
hold the same value; a privsep handle gets the error code from the
helper and leaves errno alone. Check the return value instead. With a
privsep handle, get-feature, set-feature and fdp set-events failed on a
controller device instead of falling back to all namespaces, and
ns get-id and format could report success after a failure.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
A handle opened through the privsep helper drives a local device with
the ioctl interface, only in the helper. Report it as direct, so that
callers like nvme format still rescan the namespaces, update the block
size or reset the controller after a format.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
libnvme_set_ioctl_probing() only changed the context of the client; the
helper still probed for the 64 bit passthru ioctls, e.g. for
nvme --no-ioctl-probing. Send the setting with OPEN, in the reserved
word now called options, and open the handle in a helper context
without probing. The helper rejects unknown options.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
With privilege separation, nvme-cli drops its root privileges and opens
the devices through the libnvme helper. The helper forwards the device
commands only, so the commands which do anything else must keep root
for now: the commands without a device argument (fabrics, enumeration,
keys, exclusions, configuration, registry, NBFT) and the device commands
which also access the PCI config space, the kernel log or root owned
paths (Micron vs-pcie-stats, clear-pcie-correctable-errors and
vs-internal-log, WDC and Sandisk reason-identifier and
vs-telemetry-controller-option, registry update and delete).

Add an explicit needs_root flag to struct command and set it for them.
The list is what needs to be addressed before nvme-cli can drop the
privileges for all commands. Nothing uses the flag yet.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
nvme-cli failed without a message when nvme-cli.conf exists but can't
be read. Report the file name and the error. A missing file is still
not an error.

The file holds no secrets and must be readable by every user, as nvme-cli
is going to read it after dropping its root privileges. Say so in
nvme-cli.conf(5).

Signed-off-by: Daniel Wagner <dwagner@suse.de>
When root runs nvme-cli on behalf of a user and the command does not
need root, start the libnvme privsep helper, drop the privileges and
open the devices through the helper. The decision runs first in main(),
on the command plugin_lookup() found, before the configuration file or
the options of the command are parsed, so nvme-cli.conf is read as the
target user.

The privileges are dropped to SUDO_UID/SUDO_GID or to
NVME_PRIVSEP_UID/NVME_PRIVSEP_GID. The environment is trusted only when
nvme-cli runs as root, and setuid is not supported: a setuid nvme-cli
keeps running as it was started. Without any of the variables root keeps
its privileges, which leaves scripts and services unchanged. The
supplementary groups become the target group only; looking up the ones
of the target user would run NSS as root. If the helper cannot be
started or the privileges cannot be dropped, nvme-cli fails instead of
running privileged.

Commands with an MI device (mctp:<net>,<eid>) keep root, as MI uses the
MCTP sockets directly. A command which needs root does not run once the
privileges are dropped, should a deprecated command forward to one.

The code is the same without privilege separation support, except that
it never starts a helper. For the tests, NVME_PRIVSEP_TEST_HELPER names
a helper which an unprivileged nvme-cli uses as well; it is ignored as
root and in secure-execution mode.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Add a test for the privilege separation of nvme-cli: device commands
start the helper and the helper opens the devices, while commands which
keep root, MI devices, help and version don't. The test needs
NVME_PRIVSEP_TEST_HELPER and is skipped when run as root, which ignores
it. In privsep builds, also run the mock CLI tests (print, pif-sts,
init-pi-tags, dry-run, sel, host-metadata, discovery-logs, sed) a second
time with the commands going through the helper.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
@dwsuse
dwsuse marked this pull request as ready for review October 20, 2026 09:37
@dwsuse
dwsuse requested a balanced review from Copilot October 20, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@igaw
igaw merged commit ce148c3 into linux-nvme:master Oct 20, 2026
31 checks passed
@dwsuse
dwsuse deleted the privsep-cli branch October 20, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants