Skip to content

libnvme/tests/ioctl: run the mock ioctl tests on Windows - #3815

Open
bbusacker wants to merge 6 commits into
linux-nvme:masterfrom
Micron-TPG-OSS:ioctl-tests-on-windows
Open

libnvme/tests/ioctl: run the mock ioctl tests on Windows#3815
bbusacker wants to merge 6 commits into
linux-nvme:masterfrom
Micron-TPG-OSS:ioctl-tests-on-windows

Conversation

@bbusacker

Copy link
Copy Markdown
Collaborator

The libnvme/tests/ioctl suite is the only place command-building is checked
without a device, and it has been Linux-only because both halves of the harness
are: the mock is injected with LD_PRELOAD, and it intercepts the NVMe passthru
ioctl. Windows has neither.

This series adds a Windows interception layer so the existing identify,
features and logs tests run there against the same expectations, and fixes
the two result-handling bugs the new coverage exposed.

How it works

libnvme reaches the driver through DeviceIoControl. --wrap cannot intercept
that -- libnvme is a DLL, so the call is bound through its own import thunk at
load time and never passes through a symbol the test executable links. What does
work is rewriting the thunk: iat-hook.c patches libnvme's Import Address Table
slot, which redirects the call without touching any code and leaves other modules
(including libnvme's own device enumeration) alone.

mock-win.c then decodes the Windows IOCTL back into the NVMe command it
represents and matches it against the next expected struct mock_cmd, exactly as
mock.c does. That forward translation in ioctl-win.c is lossy, so each decoder
records which fields it actually recovered -- and a bit mask where a field
survives only partly, e.g. Windows' 4-bit LogSpecificField against NVMe's 7.
Only recovered fields are compared; a discarded field is skipped rather than
compared against zero, which would turn a real mismatch into a false pass.

Where Windows genuinely cannot behave identically, the test says so instead of
being skipped: win_err and win_no_ioctl on struct mock_cmd, read through
mock_err() / mock_ok() / mock_result(). On Linux every one of those resolves
to the value the test asserted before. The mock rejects an expectation libnvme
could not have produced, so an undeclared divergence fails loudly.

libnvme/tests/ioctl/README.md documents all of this, including what a new
decoder has to do.

Two fixes

  • The Windows passthru entry points only assigned cmd->result on paths that
    retrieved completion data, leaving stale caller data in the field for any
    command that failed early. Linux always writes it back. Now both do.
  • sndk_do_sn861_drive_resize() copied admin_cmd.result out unconditionally,
    including on error paths, and its caller left the receiving variable
    uninitialised.

Scope

The other ioctl tests stay off Windows. They exercise families that reach the
driver by other routes -- SCSI CDBs for read, write and flush, dedicated
firmware, format and sanitize IOCTLs -- or that libnvme does not implement on
Windows at all. Each needs its own decoder, so they are left disabled rather than
reporting a pass they didn't earn.

Verification

  • Linux (Ubuntu 24.04, gcc): 119/119 tests pass, and per-test verdicts are
    identical to upstream/master -- the only difference in the list is
    registration order, since subdir('ioctl') moved ahead of subdir('sysfs').
  • Windows (MSYS2 UCRT64, gcc 16.1.0): 55 ok / 0 fail / 1 skipped, 0 compiler
    warnings, with libnvme - identify, libnvme - features and libnvme - logs
    passing.
  • Each of the six commits builds standalone on Linux.
  • checkpatch: 0 errors. Five of six commits are clean; the nine warnings on the
    last are all in mock-win.c and deliberate -- a Windows API function-pointer
    typedef (explained in a comment at the declaration), long fail() message
    strings, the -ENOSYS -> ERROR_CALL_NOT_IMPLEMENTED errno mapping, and a
    "falls through" phrase in prose that isn't a switch fallthrough.
  • The IAT approach was verified on both x86_64 and aarch64.

The Linux passthru ioctls always write the CQE result back, so callers can
read cmd->result after the call regardless of what it held on entry. The
Windows implementation only assigns cmd->result on the paths that manage to
retrieve completion data, which leaves whatever the caller happened to have
in the field for every command that fails early -- an unsupported opcode, a
rejected Command Set Identifier, or a failed IOCTL.

Zero it in both entry points so the two platforms agree: after a failed
command, cmd->result reads back as 0 rather than as stale caller data.

Signed-off-by: Brandon Busacker <bbusacker@micron.com>
sndk_do_sn861_drive_resize() copies admin_cmd.result out
unconditionally, including on the error paths where no completion data
was retrieved, and its caller declares the receiving variable
uninitialised. Copy the result only when the command succeeded, and
initialise the caller's variable, so a failed resize cannot be
reported with a fabricated result.

Signed-off-by: Brandon Busacker <bbusacker@micron.com>
The %m conversion is a glibc printf extension. It is not in C, and
mingw's printf does not implement it, so these diagnostics would emit
a literal "%m" once these tests are built for Windows. Call
strerror(errno) explicitly and include <string.h> (and <errno.h> in
logs.c) for it.

Signed-off-by: Brandon Busacker <bbusacker@micron.com>
Several of these tests assert an exact error or result value that a
non-Linux implementation cannot reproduce. Windows has no passthru ioctl
that carries a Command Set Identifier, so libnvme rejects those commands
before issuing any IOCTL; the Windows IOCTLs report a failed command as
EIO rather than the NVMe status code; get-features sets DNR where Linux
does not; and no completion data is available for a failed command, so
the result reads back as 0.

Rather than #ifdef the assertions, carry the difference in the mock:
win_err overrides the expected error on Windows, win_no_ioctl says the
command never reaches the mock layer at all, and mock_err(), mock_ok()
and mock_result() read the expectation for the platform under test.
WIN_CSI_UNSUPPORTED names the common case of a rejected Command Set.

On Linux every one of these resolves to the value the test asserted
before, so behaviour there is unchanged. The one new test,
set_error_clears_stale_result, checks that a failed command does not
leave a caller-supplied result in place; it seeds cmd->result after the
init helper precisely so it is testing the passthru entry point and not
nvme_init_set_features_async_event().

Signed-off-by: Brandon Busacker <bbusacker@micron.com>
The ioctl mock is injected with LD_PRELOAD on Linux, which Windows has
no equivalent of. ld --wrap does not substitute for it either: libnvme
is built as a DLL, so its call to DeviceIoControl is bound to its own
import thunk at load time and never passes through a symbol the test
executable could wrap.

Overwriting that thunk does work. Every PE image reaches an imported
function through a slot in its Import Address Table, so rewriting
libnvme's IAT slot redirects the call without modifying any code, and
the patch is confined to the one module -- which is what a test wants.
iat_patch() finds the slot for a named import and swaps in a
replacement, optionally handing back the original so it can be called
through or restored.

Nothing builds this yet; the mock that uses it follows.

Signed-off-by: Brandon Busacker <bbusacker@micron.com>
…dows

The ioctl tests have been Linux-only because both halves of the
harness are Linux-specific: the mock is injected with LD_PRELOAD and
it intercepts the NVMe passthru ioctl. Windows offers neither. What it
does offer is a small, fixed set of NVMe-specific IOCTLs that libnvme
drives through DeviceIoControl, which is interceptable by patching
libnvme's import thunk.

mock-win.c is that interception. It patches DeviceIoControl in the
libnvme module, decodes the Windows IOCTL back into the NVMe command
it represents, matches it against the next expected mock_cmd exactly
as mock.c does, and synthesises the reply the driver would have
returned. The decoders cover the three command families libnvme
implements on Windows today: identify, get/set features, and get log
page.

The remaining test programs stay off Windows. They exercise families
that reach the driver by other routes -- SCSI CDBs for read, write and
flush, dedicated firmware, format and sanitize IOCTLs -- or that
libnvme does not implement on Windows at all. Each needs its own
decoder, so enabling them is future work rather than a line in this
file, and they are left disabled instead of reporting a pass they did
not earn.

With the mock chosen inside ioctl/meson.build the host gate in the
parent meson.build has nothing left to decide, so it goes away.
README.md records how the two mocks differ and what a new decoder has
to do.

Signed-off-by: Brandon Busacker <bbusacker@micron.com>
.cdw11 = (TEST_FIDX << 0) | (TEST_CSI << 24),
.cdw14 = TEST_UUID,
.out_data = &expected_id,
WIN_CSI_UNSUPPORTED,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

.err = WIN_CSI_UNSUPPORTED ?

(((sizeof(expected_log) >> 2) - 1) << 16),
.cdw14 = (TEST_CSI << 24),
.out_data = &expected_log,
WIN_CSI_UNSUPPORTED,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here is it .err or .win_err?

@@ -0,0 +1,83 @@
# ioctl mock tests

@igaw igaw Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is libnvme/design folder. I suggest to add a sub folder tests and move this document there, with a different name obviously.

@igaw

igaw commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Thank for the effort. IMO the ioctl tests are not really bringing a lot to the table, they are mostly exercising the serializing/deserializing path which is platform neutral. So I am wondering if a Windows port is worth having. Don't get me wrong, I don't mind it as long it doesn't add additional maintenance burden. WDYT?

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.

2 participants