Skip to content

libnvme: no deterministic host identifier without root, or without DMI #3977

Description

@martin-belanger

libnvmf_generate_hostid() tries DMI, then device-tree, then a random UUID. DMI is the only deterministic source, and it needs root: /sys/class/dmi/id/product_uuid and /sys/firmware/dmi/entries/*/raw are mode 0400. An unprivileged caller gets EACCES and the code silently falls through to libnvme_random_uuid(). So the same machine reports a stable identifier as root and a different one on every other call as a regular user.

Why that matters needs no elaboration here: targets gate access on the host NQN, and credentials are bound to it.

The scope is narrow, since anything that opens a controller needs root anyway. It is not empty: nvme gen-hostnqn touches no device, runs fine unprivileged, and answers differently every time with nothing saying why. Library consumers see the same.

Proposal

Add /etc/machine-id as the first source:

/etc/machine-id (derived) -> DMI -> device-tree -> random

/etc/machine-id is systemd's local machine identifier: 32 lowercase hex characters, 128 bits, written at installation or first boot and constant afterwards. It has been in systemd since v20 (commit d7ccca2e, March 2011) and every major distribution ships it. Man page: https://man7.org/linux/man-pages/man5/machine-id.5.html

It is mode 0444, so it is the only deterministic source an unprivileged caller can read. It is also the only one that works everywhere. DMI comes from PC firmware, so it exists on x86 and on ARM64 servers following SystemReady, but not on POWER, s390x, or embedded ARM and RISC-V boards. The device-tree fallback does not fill that gap: PATH_UUID_IBM is /proc/device-tree/ibm,partition-uuid (tree-linux.c:36), which covers IBM POWER LPARs only. Every other machine without DMI goes straight to a random UUID, whatever its privileges.

The raw value must not be used. systemd's documentation says the machine ID "must not be exposed in untrusted environments, in particular on the network", and that a machine-tied identifier must instead be derived by hashing it under a fixed, application-specific key. A host NQN is sent to the target and is visible across the fabric. The derivation is sd_id128_get_machine_app_specific(): HMAC-SHA256 keyed by a public application id, truncated to 128 bits, with the UUID variant and version bits set.

A second problem in the same function

uuid_from_product_uuid() validates the string length only (fabrics.c:236). A machine reporting an all-zero or all-FF System UUID, which is common on virtual machines and whitebox boards, gets that as its identifier, shared with every identical machine. Two hosts, one NQN, which is worse than a random value.

This validation used to exist. PR #654 (2020), which is where the DMI-first ordering came from, checked the UUID format and screened for fake vendor UUIDs by rejecting a value where one character dominates, which catches all-zeros and all-FFs. Neither check came along when the logic moved into C. Worth restoring, along with a log line naming the source that won.

Implementation

sd_id128_get_machine_app_specific() is a public libsystemd API, but I would not link it here. libnvme builds for Windows and for systems without systemd, and nvme-cli links libsystemd only for nvme-discoverd. Lifting the implementation avoids that, with precedent: shr_parse_time() in shared/time-util.c came from systemd, and shared/ carries the same LGPL-2.1-or-later license. systemd uses its own SHA-256 and HMAC rather than OpenSSL, so lifting also avoids gating this on CONFIG_OPENSSL, which shr_hmac_sha256() sits behind today.

Open question: should machine-id outrank DMI? machine-id identifies an installation, so it survives package upgrades and distribution upgrades but changes if the machine is reimaged; DMI identifies hardware and survives both. I lean toward machine-id first anyway, because with DMI first the same command returns different identifiers as root and as a normal user, and that split is silent.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions