Skip to content

fuzz: fix the runner, fix 7 device-data bugs, fuzz more core commands - #4225

Merged
igaw merged 15 commits into
linux-nvme:masterfrom
dwsuse:fuzz-device-coverage
Oct 20, 2026
Merged

igaw merged 15 commits into
linux-nvme:masterfrom
dwsuse:fuzz-device-coverage

Conversation

@dwsuse

@dwsuse dwsuse commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

A one hour scripts/build.sh fuzz run of the device target never
finished, and most of its findings were noise. This fixes the runner,
the bugs it found, and extends the device target to the core commands.

Runner

  • A libFuzzer job deadlocked in its own timeout handler and the parent
    waited forever. libFuzzer now runs under timeout(1), which signals the
    whole process group.
  • With -jobs equal to -workers a worker stopped at its first finding
    (10 of 16 within five minutes). More jobs are queued than workers.
  • The mock serves at most 1 MiB per run instead of 8 MiB: the timeouts
    were valid logs of millions of zero-filled entries, printed as 25-170 MB
    of text. Many small log pages find more than a few large ones.
  • The device target fails nvme-cli allocations above 64 MiB: a device
    claimed 800 MiB once, and valgrind ran out of memory clearing it.

Fixes (each with a regression input)

  • EOM log: misaligned descriptor access, leak when growing the log fails
    (and the same leak in the Micron telemetry log), 32-bit length overflow
  • get-lba-status: descriptors read past the buffer; the JSON output
    aborted in json-c for every non-empty answer
  • host metadata features: misaligned descriptor access (also fixes the
    host metadata mock test in sanitizer builds)
  • performance characteristics: vendor specific attribute length unbounded
  • telemetry log: every read leaked a block, plus the controller header
  • dispersed namespace log: the buffer length was truncated on 32 bit
    systems, and the list read past it (crashed the cross i386 build)
  • boot partition log: the printers got the address of the buffer
    pointer, not the buffer, since the command was added

Device target

  • An input selects its command by the exact index, so adding commands no
    longer changes what the seeds and an existing corpus do. The seeds are
    rewritten 1:1 for the new format.
  • A new argument byte feeds options like --feature-id,
    --namespace-id, --log-id, --sel and --action.
  • 46 more commands: all feat commands, get-log, get-feature,
    get-lba-status, registers/properties, id *-lba-format, telemetry
    and boot partition logs, fdp feature, zns report-zones/
    zone-mgmt-recv, sed discover, rpmb, lm, dir receive,
    io-mgmt/nvme-mi/security recv, fw commit, ns get-id.
    The first ten minute round with them found the last four bugs above.

Cross builds

  • The cross files work on Debian hosts now: plain pkg-config with
    pkg_config_libdir, QEMU_LD_PREFIX for the tests run through
    binfmt_misc, renamed from ubuntu-cross-* to cross-*. All four
    targets pass on Debian 12.
  • s390x keeps building without json-c: plain pkg-config now finds the
    container's json-c, which the old wrapper never did, and some micron
    and ocp JSON mock tests fail on big endian (logs decoded in host byte
    order, e.g. through C bitfields). Left for a follow-up.
  • The net-util test no longer expects the kernel's errors from an
    ethtool query, which qemu 7.2 does not emulate.

Note: a corpus from an earlier run is in the old input format.

dwsuse added 15 commits October 19, 2026 17:19
The EOM descriptors follow each other at the descriptor size the device
reports. An odd size places them at odd addresses, and the printers
read their 16-bit fields through a misaligned struct pointer, which
UBSan reports:

  nvme-print-json.c:2441:3: runtime error: member access within
  misaligned address for type 'struct nvme_eom_lane_desc', which
  requires 2 byte alignment

Have eom_desc_iter_next() copy the fixed part of the descriptor, and
hand the printable eye functions the eye data via eom_desc_iter_eye().
While at it, the stdout printer shows the Eye Data Length in CPU byte
order too.

Fixes: 25916bc ("nvme: Spport Phy Rx Eye Opening Measurement Log")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
get_phy_rx_eom_log() assigns the result of libnvme_realloc() to the
buffer it grows, so when the device claims a log too large to allocate
the header buffer is lost. The length is computed in 32 bits, too, and
a large descriptor size times count wraps around. The Micron telemetry
log reading has the same leak.

Keep the old buffer until the new one exists, and compute the length
in size_t.

The fuzz-device target lets ASan fail a gigabyte allocation instead of
aborting with any driver, not only with the options scripts/build.sh
passes to libFuzzer: the regression input depends on it.

Fixes: 25916bc ("nvme: Spport Phy Rx Eye Opening Measurement Log")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
With 8 MiB per run, a few bytes of fuzz input make the mock serve log
pages of millions of zero-filled entries: reachability groups and
associations, LBA status, or a hardware component log. nvme-cli prints
them as 25 to 170 MB of text, which takes up to 10 seconds with the
sanitizers, and libFuzzer reports such inputs as timeouts. None of
them was a bug.

Many small log pages find more than a few large ones: limit the data
of a run to 1 MiB.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
A one hour fuzz run never finished: the timeout handler of one
libFuzzer job deadlocked after it had printed the ALARM line, and the
libFuzzer parent waits for all jobs. Run libFuzzer under timeout(1),
which signals the whole process group, the jobs included, and kills it
a minute later if that does not help.

With -jobs equal to -workers, each worker runs a single job, and a job
ends at its first finding: in that run, 10 of 16 workers stopped within
the first five minutes. Queue more jobs than workers, the timeout ends
the run.

As libFuzzer no longer exits on its own, tell the findings by the inputs
it saved in this run. A slow unit is no finding.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The Get LBA Status data counts its descriptors in NLSD, but the buffer
holds only as many as the MNDW the user asked for. Both printers walk
NLSD descriptors and read past the buffer when the device reports more.

The JSON printer also creates each descriptor as an array and then adds
named members to it, which json-c rejects with an assertion: the JSON
output aborted for every non-empty answer. And it compared the count
in little endian.

Print as many descriptors as the buffer holds, each as a JSON object.

Fixes: 6372cd9 ("nvme-print-json: Add lba_status print function")
Fixes: 2c413c9 ("Unify print routines")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
A Metadata Element Descriptor is 4 bytes and its value, so the next one
can start at any offset, and the printers read the 16-bit length through
a misaligned struct pointer. UBSan reports it, and the host metadata
mock test fails in sanitizer builds:

  nvme-print-stdout.c:2552:13: runtime error: member access within
  misaligned address for type 'struct nvme_metadata_element_desc',
  which requires 2 byte alignment

Walk the descriptors with nvme_host_metadata_element(), which copies
the fixed part and keeps the bounds of the walk as they were.

Fixes: 4499a26 ("nvme-print: Add Controller Metadata and Namespace Metadata")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
The printers of a vendor specific performance attribute dump ATTRL bytes
of it, but ATTRL comes from the device and the attribute holds at most
4064: up to 64 KiB past the buffer are read. ATTRL is little endian
too.

Fixes: 5110bff ("nvme: add performance characteristic feature output")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
get_telemetry_log() allocates a buffer and passes it to the helpers,
which replace it with the one they allocate: every telemetry log read
leaks a block. And __get_telemetry_log_ctrl() does not free the header
it reads before it gets the whole log.

Let the helpers allocate, and free the header of the controller
initiated log with a cleanup attribute.

Fixes: 2f30d87 ("nvme: Fixed segmentation fault when getting host initiated telemetry")
Fixes: 0040a90 ("nvme: Refactor get telemetry log page helper")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
The first byte of a fuzz-device input selects the command modulo the
number of commands, so adding a command changes what most inputs do:
the regression seeds would silently test other commands, and the corpus
of earlier libFuzzer runs would be lost. Inputs which select no command
are ignored instead.

Every command still runs with fixed options, so whole branches can't be
reached, the feature or namespace a command asks for. Add an argument
byte after the variant, which a bit of the variant passes to the option
a command names.

Rewrite the seeds for the new format: the same command, variant and
device answers as before.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The device target covered the id and log commands well, but little else
of the core commands and plugins. Add the commands which print what the
device returns: the LBA format identify commands, the telemetry and
boot partition logs (written to /dev/null), get-log, get-feature of the
supported capabilities, get-lba-status, the registers and properties,
all feat commands, fdp feature, the zns zone reports, sed discover, the
rpmb, lm, directive, I/O management, NVMe-MI and security receive
commands, fw commit and ns get-id.

Pass the argument byte to the options which select what a command asks
for: the feature, namespace, log, action, select or offset. For some
commands already in the table too, like the feature ID of get-feature
and the namespace of id ns.

Add seeds for the new commands, and the inputs which found the bugs
fixed before.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The valgrind run of the fuzz suite aborted: a mutation made the OCP
persistent event log allocate and clear the 800 MiB its header claims,
and valgrind's shadow memory of it exceeded the address space limit of
the target, which valgrind shares:

  Valgrind's memory management: out of memory:
     memcheck:allocate new SecMap's request for 16384 bytes failed.

The mock answers a run with 1 MiB of data at most, so a buffer larger
than a few megabytes only ever holds zeros. Fail the libnvme_alloc(),
libnvme_realloc() and libnvme_alloc_huge() calls above 64 MiB: the
target defines them and forwards the others to libnvme, as the mock
does with open() and ioctl().

Signed-off-by: Daniel Wagner <dwagner@suse.de>
get_dispersed_ns_psub() bounds the participating NVM subsystem list to
a 32 bit length, but grows its buffer to the header and that list, a
64 bit sum which libnvme_realloc() truncates to a 32 bit size_t: the
buffer comes out a few bytes long, and the list is read past its end.
The cross i386 build of the fuzz suite crashed on it.

Fail a length which a size_t does not hold.

Fixes: 6dba527 ("nvme: add dispersed-ns-participating-nss-log command")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
get_boot_part_log() passes the address of its buffer pointer to the
printers, not the buffer: they show the bytes of the pointer as the log
header, and the binary output writes the stack around it. An i386
sanitizer build of the fuzz suite reports it as a stack buffer overflow,
on 64 bit the pointer is just large enough to hide it.

The boot partition size from the log is little endian, too.

Fixes: 079afcd ("nvme: add boot partition log support")
Signed-off-by: Daniel Wagner <dwagner@suse.de>
qemu user mode, at least up to the 7.2 of Debian 12, does not emulate
the SIOCETHTOOL ioctl and fails it with ENOSYS instead of the ENODEV or
EOPNOTSUPP of the kernel. The net-util test then fails in a cross build
which runs the tests with qemu.

Check what matters: a query of a non-existent interface fails, and one
of the loopback interface does not return a positive value.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The cross files only worked in the Ubuntu CI containers:

- They use a <triplet>-pkg-config, which Debian only has with the
  pkgconf package of the target architecture installed. i386 instead
  set PKG_CONFIG_LIBDIR when configuring, which a later reconfiguration
  by ninja lost, and the build then linked the libraries of the build
  machine. Use plain pkg-config with pkg_config_libdir set to the
  multiarch directory in the cross files.
- The tests also run the binaries through binfmt_misc, and qemu finds no
  target libc unless it is installed for the target architecture in /.
  Point qemu at the libraries of the cross toolchain.

The cross files have nothing Ubuntu specific left, drop it from their
names. While at it, fix the path of the armhf ld, and make armhf the
default target: there is no file for "arm".

Document the packages a Debian host needs.

With plain pkg-config, the s390x build finds the json-c of the CI
container, which the <triplet>-pkg-config there did not, and runs the
JSON based mock tests on big endian for the first time. Some of the
micron and ocp ones fail, the plugins decode logs in host byte order.
Build s390x without json-c, as before, until that is fixed.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
@dwsuse
dwsuse force-pushed the fuzz-device-coverage branch from 1f7d2e3 to 47676f1 Compare October 19, 2026 19:29
@igaw
igaw merged commit 5184dee into linux-nvme:master Oct 20, 2026
39 checks passed
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