Skip to content

libnnvme, nvme-cli: Discover rewrite - #3828

Open
martin-belanger wants to merge 9 commits into
linux-nvme:masterfrom
martin-belanger:discover-rewrite
Open

libnnvme, nvme-cli: Discover rewrite#3828
martin-belanger wants to merge 9 commits into
linux-nvme:masterfrom
martin-belanger:discover-rewrite

Conversation

@martin-belanger

@martin-belanger martin-belanger commented Aug 12, 2026

Copy link
Copy Markdown

Rewrites libnvme's Discovery Controller walk (_nvmf_discover(), libnvmf_discover(), libnvmf_discover_nbft()) and its supporting connection-resolution code in fabrics.c. This backs nvme discover, nvme connect-all, and NBFT boot discovery.

Four main things in here:

  1. Several real bugs, found and fixed along the way (list below).
  2. NBFT discovery now walks through the same code nvme discover/connect-all use, instead of a second, forked implementation of the same walk.
  3. --force renamed to --no-reuse (kept as a deprecated alias): --force's original job was working around a persistence bug that's since been fixed elsewhere, so what's left is only its narrower meaning, "never reuse an existing connection." This is also the one commit that touches more than one file -- every other commit is mostly just fabrics.c.
  4. A new TID-keyed visited list, so the Discovery Log Page walk tracks every Discovery Controller it has already visited in this walk -- closes a gap where a referral graph with more than one path to the same DC, or an outright cycle, could be walked more than once, or skipped incorrectly.

Bugs found and fixed along the way (mostly NBFT related), none part of the original plan, all found while working through the surrounding code:

  • _nvmf_discover() never fired hooks.connected for anything it discovered and connected itself, referrals or IOC entries -- connect-all has never reported per-device progress for its own discovery-driven connects, even though the hook exists and fires correctly everywhere else.
  • Three fctx->hooks.* calls (connected, already_connected, decide_retry in libnvmf_connect()) had no NULL check, even though the hooks are documented-optional and every other call site in the file guards them. Any external libnvme caller passing NULL for a hook it doesn't need would crash.
  • NBFT: An NBFT Discovery Descriptor's host_iface was freed before its own walk started, then read (use-after-free) by every connect made during that walk.
  • NBFT: The Discovery Descriptor connection's own DHCP retry cleared the destination address instead of the local one -- a retry that could never succeed.
  • NBFT: read_discovery() (nbft.c) treated an absent DC NQN reference as a parse failure, silently dropping the whole Discovery Descriptor. Per the Boot Specification, absent just means "use the well-known NQN" -- the common case, not a malformed table.
  • NBFT: Once that parsing bug was fixed, libnvmf_discover_nbft() still never read the parsed NQN when present, always connecting with the well-known one instead.

Commits, in order. Commit 5 is the substantial one -- the actual rewrite; the rest are mostly small and mechanical:

  1. 62779e721 -- stop handing per-entry context clones to hooks/recursion (a real unsoundness: the clone shallow-aliases owned pointers a hook could free via a public setter)
  2. 0f5e6d8a4 -- unify the four resolve-or-create-a-DC paths into one dc_open()
  3. a1150d09d -- rename --force to --no-reuse (kept as a deprecated alias); the old name never described what it did
  4. 02c75ab01 -- identify a discovery self entry by its spec-defined SUBTYPE instead of a pointer-identity coincidence
  5. 32e6c0bae -- the core rewrite: depth-first walk with a visited-set and depth cap, dc_decide() extraction, and its first unit tests
  6. ac3caf4b6 -- fold NBFT's forked walk onto the same machinery (UAF and dead-retry fixes included)
  7. 56b9f0221 -- fire hooks.connected for discovery-walk connects
  8. 663dfcca5 -- NBFT Discovery Descriptor NQN parsing and consumption fixes
  9. 9a703c7f5 -- guard the three unchecked hook calls

Verification: every commit builds and tests standalone; -Dwerror=true and -Dfabrics=disabled both clean throughout; full unit suite green, including the 9 new dc_decide() cases. Live-smoke-tested nvme discover against a real TCP discovery controller. Not tested against a real NBFT boot environment.

Martin Belanger added 9 commits August 12, 2026 06:51
_nvmf_discover() and nbft_discovery()/nbft_connect() clone the
context per log entry, then pass that clone to hooks and to the
next recursion level. The clone shallow-copies hostnqn, hostid,
tls_key, and nbft_path, so a hook freeing one via a public setter
would corrupt the real context.

Pass the real context to hooks and recursion instead. Also move
set_discovery_kato() back to before the connect, restricted to
discovery-type entries: referral controllers were connecting
without a keep-alive timeout, only reaching grandchildren through
the clone's inheritance chain.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
discover_lookup_ctrl_by_device() and discover_lookup_ctrl() still
wrote fctx->persistent = NO on their two fallback paths (device
not found, not a discovery ctrl) -- the same bug class already
fixed for the FORCE overrides, just missed since these aren't
FORCE.

Merge both into dc_open(), which never touches fctx->persistent
and reports DC_OWNED/DC_BORROWED instead of the already_connected
bool. nvmf_create_discovery_ctrl() stays separate since
libnvmf_discover_nbft() also calls it directly.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
--force used to do double duty: force persistence, and skip
reusing an existing connection. Now that --persistent=force
exists as its own explicit option, --force only ever means the
latter -- so the name no longer matches what it does.

Add --no-reuse as the real name; keep --force as a deprecated
alias so existing scripts and discovery.conf files keep working.
Rename the backing fctx->force field and its accessors to match,
and check_ctrl_owner()'s force parameter, which shares the same
concept.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
_nvmf_discover() found c's own discovery log entry by checking
whether lookup_ctrl() resolved back to c. Simpler and more direct
to just check the entry's SUBTYPE for NVME_NQN_CURR (03h), which is
exactly what that field is for.

Self entries never reach nvmf_connect_disc_entry() or
dc_should_connect() now, so the SUBTYPE 03h case and the DUPRETINFO
check in those two functions were dead code, removed.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
_nvmf_discover() skipped an already-connected referral outright: no
DLP fetch, so anything only reachable through it was silently
dropped. Its only cycle protection was an incidental side effect of
looking up live connections, not an explicit guard.

Split into two passes per DC: find and act on the self entry
(SUBTYPE 03h) first, since it is the only place a DC's own EPCSD is
reported, then walk referrals and NVM subsystem entries. Add a
whole-walk TID-keyed visited set (dc_visited) so a DC is walked once
regardless of how many paths reach it, and an 8-level depth cap per
spec.

The visited set also decouples dedup from whether a connection is
still open, which is what makes it safe to disconnect a DC right
after its own DLP fetch instead of waiting for the whole walk to
finish -- the old code could not do this, since a torn-down DC
disappeared from the live connection lookup it relied on for dedup.

Extract the disconnect decision into dc_decide(), a pure function of
a DC's own facts and policy, with unit tests (test_dc_decide()) --
the first tests this code has ever had.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
nbft_discovery() was a forked copy of _nvmf_discover()'s per-DC walk,
missing the depth cap and visited-set the general path just gained.
Replace it with dc_open()/dc_walk(), the same machinery discover and
connect-all use, via a new private connect_leaf hook so NBFT's
leaf-connect quirks (DHCP retry, firing hooks.connected) stay
NBFT-only without forking the walk itself.

NBFT never honors --no-reuse (dc_open() gains honor_no_reuse) and
never respects --persistent (fctx->persistent forced to NO before the
walk): boot discovery is a one-shot operation where reusing an
existing connection is strictly better, and neither flag was ever
consulted by the old code either.

Two real, pre-existing bugs found and fixed along the way: host_iface
was freed before nbft_discovery()'s walk read it from every per-entry
connect during that DC's DLP walk; and the Discovery Descriptor
connection's own DHCP retry cleared traddr (the destination) instead
of host_traddr (the local address), which cannot ever succeed.

Also added lookup_live_ctrl(), deduplicating a lookup_ctrl() + name
check repeated at four call sites, one of them new here.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
_nvmf_discover()'s NVME_NQN_NVME and NVME_NQN_DISC branches connect
new controllers via nvmf_connect_disc_entry() directly, without ever
calling fctx->hooks.connected(). The hook is registered by every
discover/connect-all invocation path (src/fabrics.c's hook_connected,
which prints or JSON-emits per-device connect output), and already
fires correctly for a direct "nvme connect" and for NBFT's own
leaf-connect path -- just not here, so connect-all has never reported
per-device progress for anything it discovers and connects itself.

Fire it after a successful connect in both branches, guarded to skip
the NVME_NQN_NVME case when a connect_leaf hook is set: NBFT's own
hook already fires hooks.connected internally, and firing it again
here would double it.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
read_discovery() treated an absent DCNQNHOR (Discovery Controller NQN
Heap Object Reference) as a parse failure, dropping the whole
Discovery Descriptor silently. Per the NVMe Boot Specification, a
DCNQNHOR cleared to 0h just means "no unique NQN, use the well-known
one" -- the common case, not a malformed table. Tolerate -ENOENT
there specifically; every other error from the same heap-object
lookup still fails the descriptor as before.

Even for a Discovery Descriptor with a real DCNQNHOR,
libnvmf_discover_nbft() never read the parsed struct
libnvmf_discovery.nqn, always connecting with the well-known NQN
instead. Use it when present.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
libnvmf_connect() and libnvme_add_ctrl() called fctx->hooks.connected,
already_connected, and decide_retry directly, with no NULL check.
libnvmf_context_create() accepts NULL for any of the three with no
validation, and every other call site in this file that invokes the
same hooks (dc_already_connected(), nbft_connect(), the connect_leaf
path) already guards them. libnvmf_connect() is a public API function,
so any external caller passing NULL for a hook it doesn't need -- a
normal, otherwise-supported pattern -- would crash the first time that
code path fires.

decide_retry's guard defaults to "don't retry" when unset, via the
same short-circuit && the other two use as an unconditional guard.

Signed-off-by: Martin Belanger <martin.belanger@dell.com>
@martin-belanger

Copy link
Copy Markdown
Author

Looks like the windows build is timing out Error: socket hang up

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.

1 participant