Skip to content

plugins: bound the string buffers - #4219

Merged
igaw merged 21 commits into
linux-nvme:masterfrom
dwsuse:plugin-string-audit
Oct 17, 2026
Merged

igaw merged 21 commits into
linux-nvme:masterfrom
dwsuse:plugin-string-audit

Conversation

@dwsuse

@dwsuse dwsuse commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

An audit of sprintf(), strcpy(), strcat(), strncpy() and sscanf() in the plugins. All of the first four are gone from plugins/; the remaining sscanf() calls only match device names (nvme%dn%d) or skip with %*s, so they write no strings.

New helpers in shared/ first:

  • shr_strscpy(), shr_strscat(): copy/append, always terminated, -E2BIG on truncation
  • shr_snprintf_append(): append formatted text and track the length (argconfig's buf_add(), which now uses it)
  • shr_bin_to_hex(): bytes as hex, SHR_HEX_UPPER / SHR_HEX_REVERSE (little-endian GUIDs)

Then one commit per plugin. Most of it is snprintf(buf, sizeof(buf), ...) and the helpers instead of unbounded calls. Real bugs found on the way:

  • scaleflux: the controller name was strcpy()ed into an 8-byte buffer (nvme10n1 overflows it); the AER sysfs files were read() into a buffer without a terminator before sscanf(), and the fatal path closed the fd twice.
  • solidigm: workload-tracker strcat()ed field names onto an uninitialized buffer.
  • virtium: the serial and model numbers (fixed-size, unterminated identify fields) were printed with %s, reading past a full field.
  • wdc/sandisk: the C0 log page GUID was printed byte by byte with %x, dropping the leading zero of bytes below 0x10. It is printed with two digits per byte now (a visible output change, only where it was wrong).
  • get-feature (Controller/Namespace Metadata, src/nvme-print-*): the element value was strncpy()ed into an uninitialized buffer and printed unterminated, the next element was found without le16_to_cpu(), and a large element length walked past the 4096-byte structure. The walk is bounded now, in the normal and JSON output; new test host-metadata-cli.
  • exclusion: remove read the menu choice with sscanf("%zu"), so 3abc deleted entry 3. It is parsed with shr_parse_u64() now.

Output is otherwise unchanged: I replayed 6450 generated device answers through all 129 commands of the fuzz-device harness with the old and new binary and the output was identical, apart from one get-feature --verbose case whose output differed between two runs of the old binary: that is the metadata walk fixed in the last commit, and it is deterministic now. The help of all commands is byte-identical.

Every commit builds and passes the tests on its own; checkpatch is clean; nojson and Windows cross builds pass.

dwsuse added 21 commits October 17, 2026 14:16
The plugins build strings with strcpy(), strcat() and sprintf() into
fixed buffers, which overflow if the input is longer than expected.

Add shr_strscpy() and shr_strscat(), which copy or append a string and
cut it off at the size of the buffer, always terminated, and report the
cut with -E2BIG, and shr_snprintf_append(), which appends formatted
text the same way and keeps track of the length, like the buf_add() of
argconfig.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
buf_add() moved to shared as shr_snprintf_append(). The help output is
unchanged.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Several plugins print a GUID or another byte array as hex with a loop
of sprintf(p, "%02x") into a buffer whose size the loop never checks.

Add shr_bin_to_hex(), which writes the bytes as hex digits in lower or
upper case and fails with -E2BIG if the buffer is too small.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf(), shr_strscpy() and shr_bin_to_hex() instead of sprintf()
and strncpy(), which don't check the size of the buffer or leave it
unterminated.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf() and shr_strscpy() instead of sprintf() and strcpy(), and
pass getcwd() the size of the buffer instead of a number.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
setup_file() wrote the file name with sprintf() into a buffer whose size
it didn't know. Pass the size and use snprintf().

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The EUI-64 and NGUID were written byte by byte with sprintf() into a
buffer whose size the loop didn't check.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The helpers wrote the NGUID, the sizes and the device name with
sprintf() and strncpy() into buffers whose size they didn't know,
strncpy() possibly unterminated. Give the buffers a defined length and
use shr_bin_to_hex(), snprintf() and shr_strscpy().

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf(), shr_strscpy() and shr_bin_to_hex() instead of sprintf(),
strcpy() and strcat(): the GUIDs and the debug log size are written with
shr_bin_to_hex(), the FIFO strings are copied with memcpy() and the
reserved fields are appended with shr_snprintf_append(), each into a
buffer whose size is checked. The output is unchanged.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf() instead of sprintf(), and print the names of the 64 bit
attributes directly instead of copying them into a small buffer first.
The output is unchanged.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf() instead of sprintf() for the log page and drive info
strings; micron_readFixedBytesFromBuffer() takes the size of the
datastr of hyperscale_BSSD_nand_stats, which all its callers pass.
Copy the fixed size identify fields and the file path prefix with
memcpy() of their size instead of strncpy() with a number. The output
is unchanged.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf(), shr_strscpy() and shr_strscat() instead of sprintf(),
strcpy(), strncpy() and strcat(), and pass wdc_get_commit_action_bin()
the size of the buffer. The 128-bit GUIDs are read with
shr_get_unaligned_le64() instead of an unaligned load.

stringify_log_page_guid() printed each byte of the C0 log page GUID
with "%x", which dropped the leading zero of the bytes below 0x10, so
the printed GUID was too short and ambiguous. It prints two digits per
byte with shr_bin_to_hex() now. The other output is unchanged.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf(), shr_strscpy() and shr_strscat() instead of sprintf(),
strcpy(), strncpy() and strcat(), and pass sndk_get_commit_action_bin()
the size of the buffer. The 128-bit GUID of the extended SMART log is
read with shr_get_unaligned_le64() instead of an unaligned load.

sndk_stringify_log_page_guid() printed each byte of the C0 log page
GUID with "%x", which dropped the leading zero of the bytes below 0x10.
It prints two digits per byte with shr_bin_to_hex() now. The other
output is unchanged.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf() instead of sprintf() for the file path and the request
message.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
Use snprintf() with the path formats as literals, which the compiler
checks, instead of sprintf() with formats in variables.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The latency histogram units are only ever string literals: point to them
instead of copying them with strcpy() into three byte buffers. Copy the
firmware version with shr_strscpy().

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The status command copied the controller name with strcpy() into an
8 byte buffer, which a name like nvme10n1 already overflows. It read the
PCIe AER files with read() into a 512 byte buffer without terminating
it, so sscanf() could read past the end, and closed the AER file a
second time when the fatal error count couldn't be parsed.

Make the name buffer large enough and copy it with shr_strscpy(), read
the AER files with shr_read_all(), which terminates the buffer, drop
the second close(), and copy the other strings with shr_strscpy()
instead of strcpy() and strncpy().

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The smart log and header lines were built with strcpy() and strcat()
into the caller's buffer without its size, through a temporary buffer,
and the file name with strcat() into a static buffer. The hex dump
helper didn't know the size of its output either.

Pass the sizes and append with shr_snprintf_append(), write the hex
dumps with shr_bin_to_hex(), build the file name with one snprintf()
and copy the header strings with shr_strscpy(). The serial number and
model number are fixed size fields without terminator: print them with
their size instead of %s, which read past a field which is full.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
workload-tracker appended the names of the trigger fields with strcat()
to field_options, which was never initialized, so the list in the error
message started with whatever was on the stack and could overflow the
buffer.

Start the list with shr_strscpy() and append with shr_strscat(), bounded
by the size of the buffer, in join_fields() and join_options().

Signed-off-by: Daniel Wagner <dwagner@suse.de>
remove read the number of the entry with sscanf("%zu"), which takes
"3abc" as 3 and a negative number as a huge one, so a typo could delete
an entry.

Parse the answer with shr_parse_u64() up to the number of entries,
whitespace around it trimmed: anything else cancels.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
The Controller and Namespace Metadata features are printed element by
element. The value was copied with strncpy() into an uninitialized
buffer, which left it unterminated, so the output included whatever was
on the stack. The next element was found with the length field without
le16_to_cpu(), and nothing stopped the walk at the end of the 4096 byte
structure, so a large length made it read and print past the buffer.

Stop at the end of the structure, cut the length of a value off there
and print it with its length, in the normal and the JSON output.

Signed-off-by: Daniel Wagner <dwagner@suse.de>
@dwsuse
dwsuse force-pushed the plugin-string-audit branch from 98ad51c to 020e510 Compare October 17, 2026 14:51
@igaw
igaw merged commit 1be05bc into linux-nvme:master Oct 17, 2026
34 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