From 099ab36520f0b833cee724c41306848e93a97d92 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:23:03 +0530 Subject: [PATCH 01/12] Fix wrong-size free, use-after-free and double-free in kmem_vasprintf kmem_vasprintf() allocated size + 1 bytes and, on its error path, freed the same pointer with size - one byte short of what was allocated - then returned that freed pointer to the caller regardless: ptr = kmem_alloc(size + 1, KM_SLEEP); r = spl_vsnprintf(ptr, size + 1, fmt, ap); if ((r < 0) || (r > size)) { kmem_free(ptr, size); r = -1; /* r is never read again */ } ... return (ptr); Three separate faults on one path. The wrong-size free is the serious one: kmem_free() selects the cache from the size it is given, so a buffer belonging to kmem_alloc_384 is returned to kmem_alloc_256's free list. kmem_flags is 0 in every shipping driver configuration, so there are no buftags, kmem_free() validates nothing, and the buffer is handed out again later from the wrong cache - silent allocator corruption that surfaces far from its cause. This is the same failure mode as the KMERR_BADCACHE panic tracked under SSV-26896, and it has previously been seen to crash nvlist and ABD teardown long after the bad free. r is then set to -1 and never read, so the freed ptr is returned, and the caller frees it a second time. The companion kmem_asprintf() in the same file is already correct - it has no free path at all - so only this one was missed. Fix: because spl_vsnprintf() returns the length the result requires rather than a truncation flag, one measuring call sizes the buffer exactly and the write cannot truncate. That removes the retry, the error branch and the free entirely, so none of the three faults has anywhere left to live. The INT_MAX guard goes with it: spl_vsnprintf() caps its own growth at SPL_VSNPRINTF_PROBE_MAX (1 MiB) and returns a negative past that, so a measurement anywhere near INT_MAX was already unreachable. ap is reused for the write without a copy, which is what the existing code did and is correct here: on x64 va_list is a plain pointer passed by value, so a callee cannot advance the caller's copy - the same assumption spl_vsnprintf() documents for its own internal copies. A negative measurement degrades to an empty string rather than NULL. kmem_asprintf() was given a NULL return in an earlier commit, but this function and that one are declared in include/sys/zfs_context.h and shared with the Linux and FreeBSD ports, where KM_SLEEP cannot fail and callers do not check - kcf_spi.c:241, spl-kstat.c:576 and spl-procfs-list.c:234 all use the result directly. Preserving the never-NULL contract here is a two-line change against auditing every caller on three platforms. If that trade is decided the other way, kmem_asprintf()'s NULL return should stay and this should match it. Co-Authored-By: Claude Opus 5 --- module/os/windows/spl/spl-kmem.c | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/module/os/windows/spl/spl-kmem.c b/module/os/windows/spl/spl-kmem.c index 4fad95d648c1..8fd826337193 100644 --- a/module/os/windows/spl/spl-kmem.c +++ b/module/os/windows/spl/spl-kmem.c @@ -6759,21 +6759,27 @@ kmem_vasprintf(const char *fmt, va_list ap) { char *ptr; int size; - int r = -1; + /* + * spl_vsnprintf() returns the length the result requires, so one + * measuring call sizes the buffer exactly and there is no retry and + * no free on the success path. ap is reused for the write below: + * x64 va_list is a plain pointer passed by value, so a callee cannot + * advance the caller's copy - the assumption spl_vsnprintf() already + * documents for its own internal copies. + * + * A negative measurement degrades to an empty string rather than + * NULL. Callers of this function and of kmem_asprintf() are shared + * with the Linux and FreeBSD ports, where KM_SLEEP cannot fail and + * the result is never checked - see kcf_spi.c:241, spl-kstat.c:576 + * and spl-procfs-list.c:234, all of which use the result directly. + */ size = spl_vsnprintf(NULL, 0, fmt, ap); - if ((size >= 0) && (size < INT_MAX)) { - ptr = (char *)kmem_alloc(size + 1, KM_SLEEP); // +1 for null - if (ptr) { - r = spl_vsnprintf(ptr, size + 1, fmt, ap); // +1 for null - if ((r < 0) || (r > size)) { - kmem_free(ptr, size); - r = -1; - } - } - } else { - ptr = 0; - } + if (size < 0) + size = 0; + + ptr = (char *)kmem_alloc((size_t)size + 1, KM_SLEEP); // +1 for null + (void) spl_vsnprintf(ptr, (size_t)size + 1, fmt, ap); // +1 for null return (ptr); } From fa63e06287c42eb67974271ae96503911c032b23 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:24:19 +0530 Subject: [PATCH 02/12] Fix stack buffer overflow in printBuffer printBuffer() builds a thread-id prefix in a 1024-byte stack buffer and appends the caller's formatted message after it. The append was given the size of the whole buffer for a destination 17 bytes into it: char buf[max_line_length]; /* 1024 */ _snprintf_s(buf, sizeof (buf), _TRUNCATE, "%p: ", ...); int tmp = _vsnprintf_s(&buf[17], sizeof (buf), max_line_length, fmt, args); &buf[17] has 1007 bytes left, not 1024. A message long enough to fill the buffer therefore writes 17 bytes past its end. Compiled x64-Release, buf sits at rsp+0x40, the /GS cookie at rsp+0x440 and the caller's saved rbx at rsp+0x450, so the overrun lands on the cookie and the saved register - CWE-121, a genuine stack buffer overflow. Not reachable today: it needs roughly 1007 characters of output in a single call, and the messages on the paths with crash dumps are around 370 bytes. It is also fail-loud rather than silent, since /GS validates the cookie before the epilogue restores rbx, so it would surface as bugcheck 0xF7 STACK_BUFFER_OVERRUN. Neither makes it safe to keep. This is not the mechanism behind the KMERR_BADCACHE panic under SSV-26896 - that one restores a corrupted register with the cookie intact, which this cannot do - so this commit is not a fix for it. Worth recording how it survived: the CodeQL mustfix.qls pass changed the line above it and the line below it (both _snprintf calls) and left this one alone, and so did the independent remediation on the other branch. cpp/drivers/extended-deprecated-apis matches banned function names, and _vsnprintf_s is the recommended name, so it passes the check no matter what its size argument says. Nothing in the WHCP driver suite examines buffer arithmetic; cpp/overrunning-write, which targets this shape, lives in codeql/cpp-queries and was never in scope. Fix: bound the append by sizeof (buf) - prefix_len, the capacity that actually remains. Take prefix_len from strlen(buf) rather than the hardcoded 17, so the two cannot drift apart - "%p: " is 18 characters on x64, and the literal 17 was already one short of it. Pass _TRUNCATE as the count for consistency with the neighbouring calls, and test tmp < 0: _vsnprintf_s returns -1 on truncation, never a value >= the buffer size, so the old test was dead and its fallback unreachable. Co-Authored-By: Claude Opus 5 --- module/os/windows/debug.c | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/module/os/windows/debug.c b/module/os/windows/debug.c index be8c349be086..241d993486e2 100644 --- a/module/os/windows/debug.c +++ b/module/os/windows/debug.c @@ -126,12 +126,30 @@ printBuffer(const char *fmt, ...) va_list args; va_start(args, fmt); char buf[max_line_length]; + size_t prefix_len; + + /* + * "%p" emits 16 hex digits on x64, so the prefix is 18 characters and + * needs 19 bytes with its terminator. Take the length back from the + * buffer rather than assuming it, so the offset used below cannot + * disagree with what was actually written. + */ _snprintf_s(buf, sizeof (buf), _TRUNCATE, "%p: ", PsGetCurrentThread()); - - int tmp = _vsnprintf_s(&buf[17], sizeof (buf), max_line_length, - fmt, args); - if (tmp >= max_line_length) { - _snprintf_s(&buf[17], 17, _TRUNCATE, "buffer too small"); + prefix_len = strlen(buf); + + /* + * The destination is &buf[prefix_len], so the capacity remaining is + * sizeof (buf) - prefix_len, not sizeof (buf). + * + * The count is _TRUNCATE: _vsnprintf_s null-terminates on truncation + * itself and returns -1, so test tmp < 0. The previous + * "tmp >= max_line_length" could never be true. + */ + int tmp = _vsnprintf_s(&buf[prefix_len], sizeof (buf) - prefix_len, + _TRUNCATE, fmt, args); + if (tmp < 0) { + _snprintf_s(&buf[prefix_len], sizeof (buf) - prefix_len, + _TRUNCATE, "buffer too small"); } KeAcquireSpinLock(&cbuf_spin, &level); From 2c415e7299b85040f92b608c03e4e28a0c4323e8 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:27:13 +0530 Subject: [PATCH 03/12] Restore the full-buffer clear for the zpoolHealthState IOCTL field The strncpy sweep converted this to spl_strlcpy, which is not the same operation: spl_strlcpy(perf->zpoolHealthState, "", sizeof (perf->zpoolHealthState)); strncpy(dst, "", n) writes n bytes - it zero-fills the entire remainder of the destination once the source is exhausted. strlcpy(dst, "", n) writes exactly one byte, the terminator, and leaves the other sizeof (zpoolHealthState) - 1 bytes holding whatever was there before. The call is a "clear this buffer" idiom, not a string copy, so the zero-fill was the whole point of it and the conversion silently dropped it. That matters here specifically because perf points into the IOCTL output buffer for IOCTL_ZFS_GET_METRICS: the structure is copied back to user mode. Leaving all but the first byte uninitialised discloses whatever the pool allocator last left in that memory. The later assignment at line 188 only overwrites the field on the is_zpool path, so the dataset path returns the buffer with just the leading NUL written. Fix: use memset(), which is what strncpy was being used for, and which states the intent plainly. Not converted back to a bounded string copy, because there is no string to copy - the source is the empty literal. The sibling strcpy on line 188 is a real string copy and correctly became a bounded spl_strlcpy in the same sweep; it is left alone. This is a class of defect, not a single site: any strncpy replaced by strlcpy loses the zero-fill, and it only matters where the destination crosses into user space or is otherwise read beyond the terminator. The sweep touched roughly forty call sites and only this one and dsl_prop.c's dodefault() have been checked for it so far. Co-Authored-By: Claude Opus 5 --- module/os/windows/zfs/zfs_ioctl_os.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/os/windows/zfs/zfs_ioctl_os.c b/module/os/windows/zfs/zfs_ioctl_os.c index 26155c33c8c4..7744417138c4 100644 --- a/module/os/windows/zfs/zfs_ioctl_os.c +++ b/module/os/windows/zfs/zfs_ioctl_os.c @@ -139,7 +139,7 @@ NTSTATUS zpool_zfs_get_metrics(PDEVICE_OBJECT DeviceObject, PIRP Irp, PIO_STACK_ perf->zpool_allocated = 0; perf->zpool_size = 0; perf->zfs_volSize = 0; - spl_strlcpy(perf->zpoolHealthState, "", sizeof(perf->zpoolHealthState)); + memset(perf->zpoolHealthState, 0, sizeof(perf->zpoolHealthState)); perf->l2arc_alloc_size = 0; perf->l2arc_space = 0; perf->special_mirror_alloc_size = 0; From c40d638a82dfbe9f18224934dd2972853d73fad1 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:28:41 +0530 Subject: [PATCH 04/12] Restore the zero-fill lost converting strncpy in dodefault Second instance of the class described in the previous commit, in shared (non-Windows-specific) code this time. dodefault() fills a caller-supplied buffer with a property's default value. The original strncpy(buf, zfs_prop_default_string(prop), numints) zero-filled the remainder of buf whenever the default string was shorter than numints, which it almost always is - most defaults are short words like "off", "none" or "on" written into a buffer sized by the caller's numints. Replacing it with spl_strlcpy keeps the bound but drops the fill, so everything past the terminator is left holding whatever the allocation previously contained. buf here is not internal scratch: dodefault() is reached from dsl_prop_get_ds() and dsl_prop_get_dd(), and the result travels back out through the property nvlist to "zfs get". Uninitialised heap bytes past the terminator would be sent to userland with it. Fix: bzero() the buffer before the copy. bzero rather than memset to match the surrounding convention in this file and in the shared ZFS sources generally. The numeric branch below writes a full uint64_t and needs no equivalent. Co-Authored-By: Claude Opus 5 --- module/zfs/dsl_prop.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/module/zfs/dsl_prop.c b/module/zfs/dsl_prop.c index 75afbaf78de3..4fb2be028b91 100644 --- a/module/zfs/dsl_prop.c +++ b/module/zfs/dsl_prop.c @@ -58,6 +58,13 @@ dodefault(zfs_prop_t prop, int intsz, int numints, void *buf) if (intsz != 1) return (SET_ERROR(EOVERFLOW)); + /* + * strncpy(), which this replaced, zero-filled the whole of + * buf once the source was exhausted; strlcpy() writes only up + * to the terminator and leaves the tail as it found it. buf + * reaches userland through "zfs get", so restore the fill. + */ + bzero(buf, numints); (void) spl_strlcpy(buf, zfs_prop_default_string(prop), numints); } else { From 3fde64a7d3b714f45a629cb76c486cb738b79064 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:29:51 +0530 Subject: [PATCH 05/12] Restore pool-quota accounting and raise-on-failure in BufferUserBuffer FsRtlAllocatePoolWithQuotaTag() was replaced with a plain pool allocation, which drops two properties the surrounding code depends on. It charges the calling process's pool quota. BufferUserBuffer() snapshots a user-supplied buffer of caller-controlled length into non-paged pool on the METHOD_NEITHER IOCTL path, so without the charge a user-mode caller can drive unbounded non-paged pool allocation with none of it accounted against the requesting process. It also raises an exception on failure rather than returning NULL, which is why the code that follows has no NULL check - there was never a NULL to check. After the swap the allocation can return NULL, and the function goes on to set IRP_DEALLOCATE_BUFFER on the Irp and hand the NULL back to its caller. The RtlCopyMemory is inside a try/except so the copy itself would be caught, but the exception code is discarded, the caller still receives NULL, and the completion path will try to free it. The documented replacement, ExAllocatePool2 with POOL_FLAG_USE_QUOTA, is not usable here: the WDK headers gate it behind NTDDI_VERSION >= NTDDI_WIN10_VB, above this project's WDK_WINVER target of 0x0601. Taking it would raise the driver's minimum supported Windows version tree-wide, which is not a decision this cleanup should be making. Fix: reproduce both effects with the lower-level primitives FsRtlAllocatePoolWithQuotaTag is itself built on - PsChargePoolQuota() before the allocation, ExAllocatePoolUninitialized() for the allocation itself (which is what clears the deprecated-API finding), and on failure PsReturnPoolQuota() followed by ExRaiseStatus(). Callers see exactly the behaviour they saw before. Uninitialized rather than Zero because the RtlCopyMemory immediately below writes all BufferLength bytes, so a zero-fill would be overwritten in full. Co-Authored-By: Claude Opus 5 --- module/os/windows/zfs/zfs_vnops_windows.c | 34 +++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/module/os/windows/zfs/zfs_vnops_windows.c b/module/os/windows/zfs/zfs_vnops_windows.c index 4b498fe41a1c..9a24431b68cb 100644 --- a/module/os/windows/zfs/zfs_vnops_windows.c +++ b/module/os/windows/zfs/zfs_vnops_windows.c @@ -2281,11 +2281,39 @@ BufferUserBuffer(IN OUT PIRP Irp, IN ULONG BufferLength) // describing the users input buffer, which we will now snapshot. // if (Irp->AssociatedIrp.SystemBuffer == NULL) { + PVOID buf; + UserBuffer = MapUserBuffer(Irp); - Irp->AssociatedIrp.SystemBuffer = - spl_ExAllocatePoolZero(NonPagedPoolNx, - BufferLength, + + /* + * FsRtlAllocatePoolWithQuotaTag(), which this replaced, does + * two things beyond allocating: it charges the calling + * process's pool quota, and it raises an exception on failure + * rather than returning NULL. That is why the code below has + * no NULL check - there was never a NULL to check. + * + * Its documented replacement, ExAllocatePool2 with + * POOL_FLAG_USE_QUOTA, is gated behind NTDDI_VERSION >= + * NTDDI_WIN10_VB in the WDK headers, above this project's + * WDK_WINVER target of 0x0601; using it would raise the + * driver's minimum supported Windows version tree-wide. + * Reproduce both effects with the lower-level primitives + * FsRtlAllocatePoolWithQuotaTag is itself built on instead. + * + * Dropping the quota charge would also let a user-mode caller + * drive unbounded non-paged pool allocation with none of it + * accounted to the requesting process. + */ + PsChargePoolQuota(PsGetCurrentProcess(), NonPagedPoolNx, + BufferLength); + buf = ExAllocatePoolUninitialized(NonPagedPoolNx, BufferLength, 'qtaf'); + if (buf == NULL) { + PsReturnPoolQuota(PsGetCurrentProcess(), NonPagedPoolNx, + BufferLength); + ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES); + } + Irp->AssociatedIrp.SystemBuffer = buf; // // Set the flags so that the completion code knows to // deallocate the buffer. From 98ce0f5d56211b4fc2802d60f05e8e8826ee6777 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:31:21 +0530 Subject: [PATCH 06/12] Bound the running offset in fletcher_4_param_get against truncation fletcher_4_param_get() formats the implementation list into a PAGE_SIZE buffer, advancing a running offset: cnt += spl_snprintf(buffer + cnt, PAGE_SIZE - cnt, fmt, ...); Two problems, both only reachable once the output approaches PAGE_SIZE. spl_snprintf() returns the length the result required, not the length it wrote - that is the POSIX contract and what makes it usable for measuring. On truncation the two differ, so cnt advances past what is actually in the buffer and past PAGE_SIZE itself. Once cnt exceeds PAGE_SIZE, buffer + cnt points outside buffer, and PAGE_SIZE - cnt is a negative int that converts to an enormous size_t when passed as the size argument - so the very next call is unbounded and writes off the end of a PAGE_SIZE allocation. The bound defeats itself precisely when it is needed. Neither is reachable with today's implementation list, which is well short of a page. Both become reachable if the list grows, and nothing in the loop notices when it does. Fix: keep the return value in a separate len, treat len < 0 or len >= the remaining room as truncation, and stop there - returning early for the first call and breaking out of the loop for the rest. That maintains cnt < PAGE_SIZE as an invariant, so buffer + cnt stays inside the buffer and PAGE_SIZE - cnt stays positive on every iteration. Truncating the listing is the correct outcome here: the caller is a kernel parameter read, cnt is returned as the byte count, and a short but well-formed list is preferable to a buffer overrun. Co-Authored-By: Claude Opus 5 --- module/zcommon/zfs_fletcher.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/module/zcommon/zfs_fletcher.c b/module/zcommon/zfs_fletcher.c index 00bb44365a5d..4255b495b951 100644 --- a/module/zcommon/zfs_fletcher.c +++ b/module/zcommon/zfs_fletcher.c @@ -897,16 +897,32 @@ fletcher_4_param_get(char *buffer, zfs_kernel_param_t *unused) const uint32_t impl = IMPL_READ(fletcher_4_impl_chosen); char *fmt; int cnt = 0; + int len; + + /* + * spl_snprintf() returns the length the result required, which on + * truncation is larger than what it wrote. Accumulating that directly + * would push cnt past PAGE_SIZE, making buffer + cnt point outside + * buffer and PAGE_SIZE - cnt a negative int that converts to a huge + * size_t, defeating the bound on the next call. Stop at the first + * truncation instead, which keeps cnt < PAGE_SIZE throughout. + */ /* list fastest */ fmt = IMPL_FMT(impl, IMPL_FASTEST); - cnt += spl_snprintf(buffer + cnt, PAGE_SIZE - cnt, fmt, "fastest"); + len = spl_snprintf(buffer + cnt, PAGE_SIZE - cnt, fmt, "fastest"); + if (len < 0 || len >= PAGE_SIZE - cnt) + return (cnt); + cnt += len; /* list all supported implementations */ for (uint32_t i = 0; i < fletcher_4_supp_impls_cnt; ++i) { fmt = IMPL_FMT(impl, i); - cnt += spl_snprintf(buffer + cnt, PAGE_SIZE - cnt, fmt, + len = spl_snprintf(buffer + cnt, PAGE_SIZE - cnt, fmt, fletcher_4_supp_impls[i]->name); + if (len < 0 || len >= PAGE_SIZE - cnt) + break; + cnt += len; } return (cnt); From 86e0814f9410be6aa1a4c7afba353ac3826cfbe5 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:36:17 +0530 Subject: [PATCH 07/12] Take __dprintf's prefix length from strlen, not snprintf's return value __dprintf() writes the "file:line:func(): " prefix into buf, then appends the caller's message immediately after it: i = snprintf(buf, size, "%s%s:%d:%s(): ", prefix, newfile, line, func); roger = spl_vsnprintf(buf + i, size - i, fmt, adx); i is used as an offset into buf, but snprintf() here is spl_snprintf(), which returns the length the result *required* rather than the length it wrote. Those differ exactly when the write truncated, and then i is larger than the buffer: buf + i points past the end of the allocation, and size - i is negative, converting to an enormous size_t as spl_vsnprintf()'s size argument - so the append is unbounded and writes off the end of the heap allocation. Not reachable as the code stands, because size was computed as the prefix length plus the body length plus one, so the prefix always fits exactly. It becomes reachable the moment that arithmetic and this call disagree - which is precisely what the earlier off-by-one fix in this function was correcting. The hazard is specific to the POSIX return contract spl_vsnprintf() now provides. Under the previous truncation-returns-negative convention, i would have gone negative instead, and buf + i would have been a wild pointer below the allocation - the same defect with the sign reversed. Fix: derive i from strlen(buf). That is the real prefix length whether or not the write truncated, it can never exceed size - 1 because the buffer is null-terminated within its own bounds, and it therefore always leaves size - i >= 1 for the append. Co-Authored-By: Claude Opus 5 --- module/os/windows/zfs/zfs_debug.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/module/os/windows/zfs/zfs_debug.c b/module/os/windows/zfs/zfs_debug.c index 36c3fd09e99b..aab54eba6148 100644 --- a/module/os/windows/zfs/zfs_debug.c +++ b/module/os/windows/zfs/zfs_debug.c @@ -254,8 +254,18 @@ __dprintf(boolean_t dprint, const char *file, const char *func, int roger = 0; va_start(adx, fmt); - i = snprintf(buf, size, "%s%s:%d:%s(): ", + (void) snprintf(buf, size, "%s%s:%d:%s(): ", prefix, newfile, line, func); + /* + * Take the prefix length from the buffer, not from snprintf()'s + * return value. spl_snprintf() returns the length the result + * required, which on truncation exceeds what it wrote - that would + * put buf + i past the end of buf and make size - i negative, which + * converts to a huge size_t for the call below. strlen() is the real + * prefix length whether or not the write was truncated, and always + * leaves size - i >= 1. + */ + i = (int)strlen(buf); roger = spl_vsnprintf(buf + i, size - i, fmt, adx); va_end(adx); From 90fc11ddb0a59d1a0864b0d779321e80921ab096 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:37:33 +0530 Subject: [PATCH 08/12] Fix zfs_dbgmsg size accounting: strlcpy bound and wrong-size kmem_free Two size-accounting defects in the same structure. Both are latent today and both are in the class of the KMERR_BADCACHE panic tracked under SSV-26896 - a kmem_free() whose size does not match its kmem_alloc() - so they are worth closing while the code is open. __zfs_dbgmsg() allocates one block holding the header and the message, and copies into the message field: int size = sizeof (zfs_dbgmsg_t) + strlen(buf); /* 32 + len */ zfs_dbgmsg_t *zdm = kmem_zalloc(size, KM_SLEEP); strlcpy(zdm->zdm_msg, buf, size); zdm_msg does not start at the beginning of that block - it sits at offsetof(zfs_dbgmsg_t, zdm_msg), which is 28 - so the room available there is size - 28, or strlen(buf) + 4. The bound passed was 28 bytes larger than the destination. It does not overrun today only because strlcpy() stops at the source length, writing strlen(buf) + 1 bytes, three inside the real capacity. Nothing states or enforces that margin, and it does not survive a change to the structure layout or to how size is computed. zfs_dbgmsg_fini() then frees with a size recomputed from the stored message rather than the one recorded at allocation: int size = sizeof (zfs_dbgmsg_t) + strlen(zdm->zdm_msg); kmem_free(zdm, size); __zfs_dbgmsg() stores the allocation size in zdm_size for exactly this reason, and zfs_dbgmsg_purge() correctly frees with it - only this one site recomputes. Any truncation in the copy above, or any later edit of zdm_msg, makes the recomputed length smaller than what was allocated, and kmem_free() is handed a size that selects a different cache. The Linux and FreeBSD ports have no equivalent of this loop: both implement zfs_dbgmsg_fini() as a call to zfs_dbgmsg_purge(0), which uses zdm_size. The duplicated loop is a Windows-port divergence, and the divergence is what allowed the two to disagree. Fix: bound the copy by the room at zdm_msg, and free with zdm_size. Collapsing the loop into zfs_dbgmsg_purge(0) to match the other ports would remove the duplication entirely, but that restructures the function and changes its locking, so it is left for a separate change. Co-Authored-By: Claude Opus 5 --- module/os/windows/zfs/zfs_debug.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/module/os/windows/zfs/zfs_debug.c b/module/os/windows/zfs/zfs_debug.c index aab54eba6148..ca14dfd96492 100644 --- a/module/os/windows/zfs/zfs_debug.c +++ b/module/os/windows/zfs/zfs_debug.c @@ -138,7 +138,13 @@ zfs_dbgmsg_fini(void) kstat_delete(zfs_dbgmsg_kstat); while ((zdm = list_remove_head(&zfs_dbgmsgs)) != NULL) { - int size = sizeof (zfs_dbgmsg_t) + strlen(zdm->zdm_msg); + /* + * Free with the size recorded at allocation, as + * zfs_dbgmsg_purge() already does. Recomputing it from the + * stored message re-derives a length that no longer has to + * match what was allocated. + */ + int size = zdm->zdm_size; kmem_free(zdm, size); zfs_dbgmsg_size -= size; } @@ -184,7 +190,12 @@ __zfs_dbgmsg(char *buf) zfs_dbgmsg_t *zdm = kmem_zalloc(size, KM_SLEEP); zdm->zdm_size = size; zdm->zdm_timestamp = gethrestime_sec(); - strlcpy(zdm->zdm_msg, buf, size); + /* + * The bound is the room at zdm_msg, not the size of the whole + * allocation: zdm_msg starts offsetof(zfs_dbgmsg_t, zdm_msg) bytes + * in, so only size minus that many bytes exist there. + */ + strlcpy(zdm->zdm_msg, buf, size - offsetof(zfs_dbgmsg_t, zdm_msg)); mutex_enter(&zfs_dbgmsgs_lock); list_insert_tail(&zfs_dbgmsgs, zdm); From f5eeaf9c3dd650ca91129dc5d6a8c36ee0ec80e0 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:56:19 +0530 Subject: [PATCH 09/12] Restore the never-NULL contract in kmem_asprintf kmem_asprintf() was given a NULL return for the case where its spl_vsnprintf() measuring call fails - the new Tier 3 exhaustion path, reachable when a format needs more than SPL_VSNPRINTF_PROBE_MAX or when the caller is at DISPATCH_LEVEL. That is an honest signal, but it is a change of contract, and the contract has callers. kmem_asprintf() and kmem_vasprintf() are declared in include/sys/zfs_context.h and implemented separately for Windows, Linux and FreeBSD. On the other two ports they allocate with KM_SLEEP and cannot fail, so no caller checks the result: module/icp/spi/kcf_spi.c:241 ks_name = kmem_asprintf(...) module/os/linux/spl/spl-kstat.c:576 parent = kmem_asprintf(...) module/os/linux/spl/spl-procfs-list.c:234 modulestr = kmem_asprintf(...) lib/libzfs/os/freebsd/libzfs_ioctl_compat.c:279,347,378 Each assigns and dereferences directly. A NULL return here converts a formatting failure into a null dereference at the call site, which is worse than the truncated string it replaced - and the call sites are in code shared with the other ports, where the possibility does not exist and a reviewer has no reason to look for it. Fix: treat a failed measurement as a zero-length result, so the buffer is still allocated and still a valid empty C string. Matches the handling in kmem_vasprintf(), so the two functions in the same file now agree, which they did not before. Hardening every caller on three platforms is the alternative and is the better long-term answer if a formatting failure ever needs to be distinguishable. It is out of scope for a deprecated-API cleanup. Co-Authored-By: Claude Opus 5 --- module/os/windows/spl/spl-kmem.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/module/os/windows/spl/spl-kmem.c b/module/os/windows/spl/spl-kmem.c index 8fd826337193..84efd7d4c403 100644 --- a/module/os/windows/spl/spl-kmem.c +++ b/module/os/windows/spl/spl-kmem.c @@ -6736,8 +6736,13 @@ kmem_asprintf(const char *fmt, ...) size = spl_vsnprintf(NULL, 0, fmt, adx); va_end(adx); + /* + * Degrade a failed measurement to an empty string rather than + * returning NULL: callers of this function do not check, because + * KM_SLEEP cannot fail. See kmem_vasprintf(), which matches. + */ if (size < 0) - return (NULL); /* honest failure, not KMEM_ZERO_SIZE_PTR */ + size = 0; size++; buf = kmem_alloc(size, KM_SLEEP); From ac316fdaf27b0346c101897c9a4fa6e5da8cc398 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:57:58 +0530 Subject: [PATCH 10/12] Raise SPL_VSNPRINTF_PROBE_MIN to 1024 to keep __dprintf off the allocator spl_vsnprintf() tries the caller's own buffer, then a stack probe, then the heap. Only the third tier allocates, and the probe was 256 bytes. That is below the size of the log lines this driver actually emits. The metaslab_load message, the one this tree has crash dumps of, is 311 characters - 38 of prefix and 273 of body - so __dprintf()'s measuring call (buf == NULL, which skips Tier 1 by construction) fell through to Tier 3 on every single emission. The common case was the allocating case, which is the opposite of what the tier structure is for. That matters beyond the wasted alloc/free pair. __dprintf() is reachable from inside the kmem allocators themselves: kmem_error() calls dprintf() directly while reporting a corrupted buffer, and spl-kmem.c and spl-vmem.c call it from many other places. Measuring a log line by allocating means the allocator's own error path re-enters the allocator. sys/types.h states the constraint for this reason - the logger must stay to a single bounded allocation, no grow-and-retry and no helper that allocates more than once. Raising the probe to 1024 keeps every realistic log line on Tier 2, so the measuring call allocates nothing and the only allocation in __dprintf() is the one it makes for the message itself. Tier 3 remains for genuinely unbounded formats such as module/lua/lstrlib.c's channel programs, where an allocation is unavoidable and the IRQL guard already covers it. Cost is 768 additional bytes of stack in spl_vsnprintf()'s frame. The dbgmsg path measured from a crash dump uses roughly 2.9 KB from taskq_thread() down to __dprintf(), against a 12 KB kernel stack, and spl_vsnprintf() is a leaf below that - so the margin is ample. Also corrects two comments the change invalidates: Tier 3 described the probe by its literal size, and Tier 2 claimed every real caller writes "under a few hundred bytes", which was the assumption that produced 256 and is not true of __dprintf(). Co-Authored-By: Claude Opus 5 --- module/os/windows/spl/spl-kmem.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/module/os/windows/spl/spl-kmem.c b/module/os/windows/spl/spl-kmem.c index 84efd7d4c403..ba72a370e473 100644 --- a/module/os/windows/spl/spl-kmem.c +++ b/module/os/windows/spl/spl-kmem.c @@ -6613,7 +6613,16 @@ kmem_asdprintf(const char *fmt, ...) return (ptr); } -#define SPL_VSNPRINTF_PROBE_MIN 256 +/* + * Sized so that Tier 2 covers every log line this driver actually emits, + * because Tier 3 is the only tier that allocates and __dprintf() - which + * measures with buf == NULL, so it always reaches at least Tier 2 - is + * reachable from inside the kmem allocators themselves. kmem_error() + * calls dprintf() directly. At 256 the metaslab_load message, 311 + * characters, fell through to Tier 3 on every emission, putting a + * kmem_alloc()/kmem_free() pair inside the allocator's own error path. + */ +#define SPL_VSNPRINTF_PROBE_MIN 1024 /* * 1 MiB: roughly 256x the largest single formatted string anywhere * in this tree today (PAGE_SIZE == 4096, in zfs_fletcher.c). No real @@ -6666,15 +6675,14 @@ spl_vsnprintf(char *buf, size_t size, const char *fmt, va_list args) } /* - * Tier 2: a small on-stack probe. Still IRQL-safe (no - * allocation) - every real caller in this tree writes a buffer - * under a few hundred bytes (the one known exception, - * module/lua/lstrlib.c's Lua channel-program formatting, is - * intentionally unbounded and falls through to Tier 3), so this - * is what makes every measure-only caller (buf==NULL, e.g. - * __dprintf's first call, kmem_asprintf(), kmem_vasprintf()) - * avoid the allocator entirely in the overwhelmingly common - * case. + * Tier 2: an on-stack probe, sized by SPL_VSNPRINTF_PROBE_MIN. + * Still IRQL-safe (no allocation), and it is what makes every + * measure-only caller (buf == NULL, e.g. __dprintf's first call, + * kmem_asprintf(), kmem_vasprintf()) avoid the allocator + * entirely. Sized to cover the log lines this driver actually + * emits rather than "a few hundred bytes" - see the constant. + * module/lua/lstrlib.c's Lua channel-program formatting is + * intentionally unbounded and still falls through to Tier 3. */ args_copy = args; ret = _vsnprintf_s(stackbuf, sizeof (stackbuf), _TRUNCATE, fmt, @@ -6683,7 +6691,8 @@ spl_vsnprintf(char *buf, size_t size, const char *fmt, va_list args) return (ret); /* - * Tier 3: only reached when even a 256-byte probe truncates. + * Tier 3: only reached when even the SPL_VSNPRINTF_PROBE_MIN stack + * probe truncates. * This is the only tier that allocates, so it is the only tier * that can violate IRQL rules (KM_SLEEP can block) - guard it * explicitly here, at the one place that actually needs it, From 99984fe12970d6ba0927c6e30884a2b3bb19a737 Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:19:19 +0530 Subject: [PATCH 11/12] Add the scoped CodeQL analysis script used to produce these findings The Must-Fix findings fixed across this branch came from a CodeQL scan that is not reproducible from anything currently in the tree - the scoping was done by hand and existed only in one working copy. Commit the script so the same scan can be re-run to verify the finding count drops, and re-run again on future changes. What it does differently from a naive scan: it traces a CMake+Ninja build of only the ZFSin target and what statically links into it (splkern, zlibkern, icpkern, luakern, zfskern, zfskern_os, zcommonkern, nvpairkern, unicodekern, zstdkern), rather than the whole tree. The user-mode tools and libraries share source files with the driver - module/zfs/*.c is also compiled into libzpool - but under different macros and headers, so a database containing both reports the same source line twice in two contexts and inflates the count. Scoping to the driver is also what the HLK Static Tools Logo Test actually requires. It then analyzes with the WHCP mustfix.qls suite and prints a summary grouped by rule and API. Worth stating plainly, because it bounds what this scan can tell us: CodeQL sees only what the build compiles. Files present in the tree but absent from the ZFSin target - spl-lookasidelist.c and module/icp/algs/blake3/blake3_impl.c today - are invisible to it, and both still contain deprecated APIs that will surface as new findings the moment either is added to the build. Functions excluded by the preprocessor are equally invisible; icp_aes_impl_get(), icp_gcm_impl_get(), zfs_vdev_raidz_impl_get() and spl-kstat.c's 32-bit compat block all contain raw sprintf or strcpy calls that are compiled out on Windows, confirmed by checking for their symbols in the built objects rather than by reading the #ifdefs. The suite is also narrower than "memory safety": mustfix.qls is a driver-certification suite whose deprecated-API query matches function names. It cannot see buffer arithmetic, which is how a genuine stack buffer overflow in printBuffer() survived two independent remediation passes - see the commit that fixes it. Running codeql/cpp-queries:codeql-suites/cpp-security-extended.qls over the same database would cover that class and is worth doing before this work is signed off. Co-Authored-By: Claude Opus 5 --- .../codeql/Invoke-CodeQLZFSinAnalysis.ps1 | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 contrib/windows/codeql/Invoke-CodeQLZFSinAnalysis.ps1 diff --git a/contrib/windows/codeql/Invoke-CodeQLZFSinAnalysis.ps1 b/contrib/windows/codeql/Invoke-CodeQLZFSinAnalysis.ps1 new file mode 100644 index 000000000000..e04c0500c25e --- /dev/null +++ b/contrib/windows/codeql/Invoke-CodeQLZFSinAnalysis.ps1 @@ -0,0 +1,187 @@ +# ***************************************************************************** +# Copyright (c) 2026 DataCore Software Corporation. All rights reserved. +# ***************************************************************************** + +<# + .SYNOPSIS + Runs CodeQL static analysis over the ZFSin kernel driver target only + (not the full CMake build) and reports the Must-Fix findings required + for the HLK "Static Tools Logo Test" (WHCP certification). + + .DESCRIPTION + 1. Builds a CodeQL database by tracing a CMake+Ninja build of ONLY the + `ZFSin` target (the driver .sys and everything statically linked + into it: splkern, zlibkern, icpkern, luakern, zfskern, zfskern_os, + zcommonkern, nvpairkern, unicodekern, zstdkern). This deliberately + excludes the user-mode tools (zfs.exe, zpool.exe, zfsinstaller.exe, + etc.) and user-mode libraries (libzfs, libnvpair, libzpool, ...) - + several of those share source files with the driver (e.g. + module/zfs/*.c is also compiled into libzpool) but under different + macros/headers, and mixing both into one database produces + confusing multi-context findings for the same source line. + 2. Analyzes the database with the WHCP `mustfix.qls` suite, writing + SARIF to the repo root. + 3. Prints a summary grouped by rule/API. + + .PREREQUISITES + - CodeQL CLI 2.20.1 unpacked to -CodeQLHome (WHCP matrix version). + - Query packs downloaded: microsoft/windows-drivers@1.8.0 and + microsoft/cpp-queries@0.0.4. + - VS2019 (vcvars64.bat) + WDK 10.0.19041 + OpenSSL-Win64 + the + prebuilt ISA-L static libs under lib\ISA-L\ - all + already required by this repo's normal CMake build. + + .EXAMPLE + .\Invoke-CodeQLZFSinAnalysis.ps1 +#> + +PARAM +( + [Parameter(HelpMessage = "Directory containing the CodeQL CLI (codeql.exe)")] + [string]$CodeQLHome = "C:\codeql-home\codeql", + + [Parameter(HelpMessage = "WHCP query suite to run")] + [ValidateSet("mustfix", "recommended", "mustrun")] + [string]$Suite = "mustfix", + + [Parameter(HelpMessage = "CMake build configuration")] + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Debug", + + [Parameter(HelpMessage = "Path to vcvars64.bat (VS2019)")] + [string]$VcVars64 = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvars64.bat", + + [Parameter(HelpMessage = "Directory for the CodeQL database")] + [string]$DatabasePath, + + [Parameter(HelpMessage = "Output SARIF path")] + [string]$SarifPath, + + [Parameter(HelpMessage = "Reuse an existing CodeQL database instead of rebuilding it")] + [switch]$ReuseDatabase +) + +$ErrorActionPreference = "Stop" + +# Repo root is two levels up from contrib\windows\codeql. +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path + +if (!$DatabasePath) { $DatabasePath = Join-Path $repoRoot "out\CodeQL\databases\ZFSin" } +if (!$SarifPath) { $SarifPath = Join-Path $repoRoot "ZFSin.codeql.sarif" } + +$buildDir = Join-Path $repoRoot "out\build\codeql-zfsin" + +function Invoke-Tool([string]$Exe, [string[]]$ToolArgs) +{ + $prevEap = $script:ErrorActionPreference + $script:ErrorActionPreference = "Continue" + try + { + & $Exe @ToolArgs 2>&1 | ForEach-Object { Write-Host $_.ToString() } + return $LASTEXITCODE + } + finally + { + $script:ErrorActionPreference = $prevEap + } +} + +$codeqlExe = Join-Path $CodeQLHome "codeql.exe" +if (!(Test-Path $codeqlExe)) +{ + $nested = Join-Path $CodeQLHome "codeql\codeql.exe" + if (Test-Path $nested) { $codeqlExe = $nested } +} +if (!(Test-Path $codeqlExe)) +{ + throw "codeql.exe not found under '$CodeQLHome'." +} + +if (!(Test-Path $VcVars64)) +{ + throw "vcvars64.bat not found at '$VcVars64'. Pass -VcVars64 explicitly." +} + +$suiteSpec = "microsoft/windows-drivers:windows-driver-suites\$Suite.qls" + +Write-Host "Repo root : $repoRoot" +Write-Host "CodeQL : $codeqlExe" +Write-Host "Query suite : $suiteSpec" +Write-Host "Build dir : $buildDir" +Write-Host "Database : $DatabasePath" +Write-Host "SARIF output : $SarifPath" +Write-Host "" + +# Fail early if the WHCP query packs are missing. +$prevEap = $ErrorActionPreference +$ErrorActionPreference = "Continue" +$qlpacks = (& $codeqlExe resolve packs 2>&1 | ForEach-Object { $_.ToString() }) -join "`n" +$ErrorActionPreference = $prevEap +if ($qlpacks -notmatch "microsoft/windows-drivers") +{ + throw "Query pack microsoft/windows-drivers not found. Run: codeql pack download microsoft/windows-drivers@1.8.0 (and microsoft/cpp-queries@0.0.4)." +} + +New-Item -ItemType Directory -Force (Split-Path $DatabasePath -Parent) > $null + +if ($ReuseDatabase -and (Test-Path (Join-Path $DatabasePath "codeql-database.yml"))) +{ + Write-Host "Reusing existing database $DatabasePath" +} +else +{ + # Build script: vcvars64 -> configure (only if needed) -> build ONLY the + # ZFSin target (not the default `all` target, which would also build the + # unrelated user-mode tools/libraries). + $buildScript = Join-Path $env:TEMP "build-zfsin-codeql.cmd" + @" +@echo off +setlocal +call "$VcVars64" +if errorlevel 1 exit /b 1 +set "CMAKE=%ProgramFiles(x86)%\Microsoft Visual Studio\2019\Professional\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" +set "NINJA=%ProgramFiles(x86)%\Microsoft Visual Studio\2019\Professional\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja\ninja.exe" +cd /d "$repoRoot" +if not exist "$buildDir\CMakeCache.txt" ( + "%CMAKE%" -S . -B "$buildDir" -G Ninja -DCMAKE_BUILD_TYPE=$Configuration -DCMAKE_MAKE_PROGRAM="%NINJA%" + if errorlevel 1 exit /b 1 +) +"%CMAKE%" --build "$buildDir" --target ZFSin --clean-first +if errorlevel 1 exit /b 1 +exit /b 0 +"@ | Out-File -FilePath $buildScript -Encoding ASCII + + if (Test-Path $DatabasePath) { Remove-Item -Recurse -Force $DatabasePath } + $code = Invoke-Tool $codeqlExe @("database", "create", $DatabasePath, "--language=cpp", + "--source-root=$repoRoot", "--command=$buildScript", "--overwrite") + if ($code -ne 0) { throw "codeql database create failed (exit $code)" } +} + +$code = Invoke-Tool $codeqlExe @("database", "analyze", $DatabasePath, $suiteSpec, + "--format=sarifv2.1.0", "--output=$SarifPath", "--rerun") +if ($code -ne 0) { throw "codeql database analyze failed (exit $code)" } + +$sarif = Get-Content $SarifPath -Raw | ConvertFrom-Json +$findings = @() +foreach ($run in $sarif.runs) { if ($run.results) { $findings += $run.results } } + +Write-Host "" +Write-Host "================ ZFSin ($Suite) ================" -ForegroundColor Cyan +if ($findings.Count -eq 0) +{ + Write-Host "clean - 0 findings" -ForegroundColor Green +} +else +{ + Write-Host "$($findings.Count) finding(s):" -ForegroundColor Yellow + $findings | Group-Object ruleId | Sort-Object Count -Descending | ForEach-Object { + Write-Host (" {0,4} x {1}" -f $_.Count, $_.Name) -ForegroundColor Yellow + } + if ($Suite -eq "mustfix") + { + Write-Host "" + Write-Host "FAILS certification until fixed" -ForegroundColor Red + } +} + +exit $findings.Count From 84a3799e16b6b50d8c3ed88be2dea8ea715bbd2d Mon Sep 17 00:00:00 2001 From: Senthil <79847390+datacore-senthil@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:27:24 +0530 Subject: [PATCH 12/12] Use the existing strlcpy/strlcat instead of duplicating them as spl_* The strncpy and strcat sweeps introduced spl_strlcpy() and spl_strlcat() as static inlines in sys/types.h, on the stated grounds that strlcpy() "has no kernel-linkable implementation here, so provide one". That premise is wrong. Both are declared in sys/sunddi.h (lines 187-188) and defined for kernel mode in module/os/windows/spl/spl-ddi.c (lines 579 and 597), and they link - the parallel remediation on SSV-26896-fix converted these same call sites to plain strlcpy/strlcat and built clean. The tree therefore carried two implementations of each function, and the user-mode half of the pair (lib/libspl/include/os/windows/sys/types.h) was already nothing but a passthrough to the very function it was said not to have. Both implementations are equivalent - each returns strlen(src), always terminates when the destination size is non-zero, and never overruns - so this is a rename, not a behaviour change. Keeping strlcpy/strlcat rather than the spl_ names matters most for the shared sources. module/zfs, module/icp and module/lua are periodically merged from upstream openzfsonwindows/openzfs, and upstream, Linux and FreeBSD all spell these strlcpy/strlcat. Every spl_strlcpy in shared code is a permanent merge conflict for no benefit - the same reasoning the vsnprintf work used to argue for a header-only fix over touching ~140 call sites. 40 call sites across 16 files, plus removal of both inline definitions and the user-mode passthroughs. Verified with a full x64-Release build of the whole tree, driver and user-mode tools, 356/356 targets. Not addressed here, but adjacent and pre-existing: lib/libspl carries its own duplicate pair, with strlcpy and strlcat defined both in os/windows/posix.c (lines 715, 733) and in strlcpy.c/strlcat.c. That is what produces the LNK4006 "already defined in posix.c.obj" warnings when libspl is archived, and it is unaffected by this change. Co-Authored-By: Claude Opus 5 --- include/os/windows/spl/sys/types.h | 50 ++--------------------- lib/libspl/include/os/windows/sys/types.h | 24 +++-------- lib/os/windows/zlib-1.2.3/gzio.c | 8 ++-- module/icp/core/kcf_mech_tabs.c | 32 +++++++-------- module/icp/spi/kcf_spi.c | 2 +- module/os/windows/spl/spl-kmem.c | 2 +- module/os/windows/spl/spl-kstat.c | 2 +- module/os/windows/spl/spl-taskq.c | 2 +- module/os/windows/zfs/zfs_ctldir.c | 4 +- module/os/windows/zfs/zfs_ioctl_os.c | 2 +- module/os/windows/zfs/zfs_vnops_windows.c | 2 +- module/zfs/dmu_send.c | 4 +- module/zfs/dsl_dir.c | 4 +- module/zfs/dsl_prop.c | 4 +- module/zfs/spa_misc.c | 2 +- module/zfs/zcp_get.c | 2 +- module/zfs/zfs_ioctl.c | 2 +- module/zfs/zio_inject.c | 2 +- 18 files changed, 48 insertions(+), 102 deletions(-) diff --git a/include/os/windows/spl/sys/types.h b/include/os/windows/spl/sys/types.h index db7b1fb545e8..751117176aa7 100644 --- a/include/os/windows/spl/sys/types.h +++ b/include/os/windows/spl/sys/types.h @@ -146,54 +146,12 @@ spl_snprintf(char *buf, size_t size, const char *fmt, ...) */ /* - * Kernel-mode strncpy() does not NUL-terminate if strlen(src) >= n, - * and zero-fills the whole remainder of the buffer if strlen(src) < n - * - neither behavior is depended on by any of this codebase's callers. - * strlcpy() is the semantically-closest safe replacement (always - * terminates, never overflows) but has no kernel-linkable - * implementation here, so provide one - mirrors lib/libspl/strlcpy.c's - * existing user-mode algorithm exactly. Callers pass n == the size of - * the destination buffer (or the intended-substring-length + 1), + * strncpy() and strcat() are replaced tree-wide by strlcpy() and + * strlcat(), which are declared in sys/sunddi.h and implemented for + * kernel mode in module/os/windows/spl/spl-ddi.c. Callers pass n == the + * size of the destination buffer (or the intended-substring-length + 1), * unlike strncpy's n == copy-length - not a drop-in same-args swap. */ -static __inline size_t -spl_strlcpy(char *dst, const char *src, size_t dstsize) -{ - size_t srclen = strlen(src); - size_t copied = (srclen < dstsize) ? srclen : dstsize - 1; - - if (dstsize != 0) { - memcpy(dst, src, copied); - dst[copied] = '\0'; - } - return (srclen); -} - -/* - * strcat() has no size parameter at all - unbounded by construction. - * strlcat() is the closest safe replacement (always terminates, never - * overflows, return value is the total length it tried to create) but - * - same as strlcpy() - has no kernel-linkable implementation here. - * Mirrors lib/libspl/strlcat.c's existing user-mode algorithm exactly. - */ -static __inline size_t -spl_strlcat(char *dst, const char *src, size_t dstsize) -{ - char *df = dst; - size_t left = dstsize; - size_t l1, l2 = strlen(src), copied; - - while (left-- != 0 && *df != '\0') - df++; - l1 = df - dst; - if (dstsize == l1) - return (l1 + l2); - - copied = (l1 + l2 >= dstsize) ? dstsize - l1 - 1 : l2; - memcpy(dst + l1, src, copied); - dst[l1 + copied] = '\0'; - return (l1 + l2); -} #ifndef ULLONG_MAX #define ULLONG_MAX (~0ULL) diff --git a/lib/libspl/include/os/windows/sys/types.h b/lib/libspl/include/os/windows/sys/types.h index fb0e153febff..815cb60cedd5 100644 --- a/lib/libspl/include/os/windows/sys/types.h +++ b/lib/libspl/include/os/windows/sys/types.h @@ -232,27 +232,15 @@ typedef uint64_t zoff_t; #include /* - * Mirrors include/os/windows/spl/sys/types.h's kernel-mode shims of the + * Mirrors include/os/windows/spl/sys/types.h's kernel-mode shim of the * same name. Several shared module/zfs, module/icp, and module/lua * source files (built both into the ZFSin kernel driver and into - * user-mode libzpool/libicp/zlib here) call these directly by name, not - * through a portable macro. In user mode, real strlcpy/strlcat - * (lib/libspl) and real, C99-conformant UCRT vsnprintf are already - * available, so these are simple passthroughs - no downlevel- - * unavailability workaround is needed here, unlike the kernel version. + * user-mode libzpool/libicp/zlib here) call this directly by name, not + * through a portable macro. In user mode a real, C99-conformant UCRT + * vsnprintf is already available, so this is a simple passthrough - no + * downlevel-unavailability workaround is needed here, unlike the kernel + * version. */ -static __inline size_t -spl_strlcpy(char *dst, const char *src, size_t dstsize) -{ - return (strlcpy(dst, src, dstsize)); -} - -static __inline size_t -spl_strlcat(char *dst, const char *src, size_t dstsize) -{ - return (strlcat(dst, src, dstsize)); -} - static __inline int spl_vsnprintf(char *buf, size_t size, const char *fmt, va_list args) { diff --git a/lib/os/windows/zlib-1.2.3/gzio.c b/lib/os/windows/zlib-1.2.3/gzio.c index 5c7ed4aaf16b..36daa59082bf 100644 --- a/lib/os/windows/zlib-1.2.3/gzio.c +++ b/lib/os/windows/zlib-1.2.3/gzio.c @@ -132,7 +132,7 @@ gz_open( if (s->path == NULL) { return (destroy(s), (gzFile)Z_NULL); } - spl_strlcpy(s->path, path, strlen(path) + 1); /* do this early for debugging */ + strlcpy(s->path, path, strlen(path) + 1); /* do this early for debugging */ s->mode = '\0'; do { @@ -1094,9 +1094,9 @@ gzerror( s->msg = (char *)ALLOC(strlen(s->path) + strlen(m) + 3); if (s->msg == Z_NULL) return ((const char *)ERR_MSG(Z_MEM_ERROR)); - spl_strlcpy(s->msg, s->path, strlen(s->path) + strlen(m) + 3); - spl_strlcat(s->msg, ": ", strlen(s->path) + strlen(m) + 3); - spl_strlcat(s->msg, m, strlen(s->path) + strlen(m) + 3); + strlcpy(s->msg, s->path, strlen(s->path) + strlen(m) + 3); + strlcat(s->msg, ": ", strlen(s->path) + strlen(m) + 3); + strlcat(s->msg, m, strlen(s->path) + strlen(m) + 3); return ((const char *)s->msg); } diff --git a/module/icp/core/kcf_mech_tabs.c b/module/icp/core/kcf_mech_tabs.c index d9f185eea8b0..a4a5380925e3 100644 --- a/module/icp/core/kcf_mech_tabs.c +++ b/module/icp/core/kcf_mech_tabs.c @@ -177,72 +177,72 @@ kcf_init_mech_tabs(void) /* Then the pre-defined mechanism entries */ /* Two digests */ - (void) spl_strlcpy(kcf_digest_mechs_tab[0].me_name, SUN_CKM_MD5, + (void) strlcpy(kcf_digest_mechs_tab[0].me_name, SUN_CKM_MD5, CRYPTO_MAX_MECH_NAME); kcf_digest_mechs_tab[0].me_threshold = kcf_md5_threshold; - (void) spl_strlcpy(kcf_digest_mechs_tab[1].me_name, SUN_CKM_SHA1, + (void) strlcpy(kcf_digest_mechs_tab[1].me_name, SUN_CKM_SHA1, CRYPTO_MAX_MECH_NAME); kcf_digest_mechs_tab[1].me_threshold = kcf_sha1_threshold; /* The symmetric ciphers in various modes */ - (void) spl_strlcpy(kcf_cipher_mechs_tab[0].me_name, SUN_CKM_DES_CBC, + (void) strlcpy(kcf_cipher_mechs_tab[0].me_name, SUN_CKM_DES_CBC, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[0].me_threshold = kcf_des_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[1].me_name, SUN_CKM_DES3_CBC, + (void) strlcpy(kcf_cipher_mechs_tab[1].me_name, SUN_CKM_DES3_CBC, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[1].me_threshold = kcf_des3_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[2].me_name, SUN_CKM_DES_ECB, + (void) strlcpy(kcf_cipher_mechs_tab[2].me_name, SUN_CKM_DES_ECB, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[2].me_threshold = kcf_des_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[3].me_name, SUN_CKM_DES3_ECB, + (void) strlcpy(kcf_cipher_mechs_tab[3].me_name, SUN_CKM_DES3_ECB, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[3].me_threshold = kcf_des3_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[4].me_name, SUN_CKM_BLOWFISH_CBC, + (void) strlcpy(kcf_cipher_mechs_tab[4].me_name, SUN_CKM_BLOWFISH_CBC, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[4].me_threshold = kcf_bf_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[5].me_name, SUN_CKM_BLOWFISH_ECB, + (void) strlcpy(kcf_cipher_mechs_tab[5].me_name, SUN_CKM_BLOWFISH_ECB, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[5].me_threshold = kcf_bf_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[6].me_name, SUN_CKM_AES_CBC, + (void) strlcpy(kcf_cipher_mechs_tab[6].me_name, SUN_CKM_AES_CBC, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[6].me_threshold = kcf_aes_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[7].me_name, SUN_CKM_AES_ECB, + (void) strlcpy(kcf_cipher_mechs_tab[7].me_name, SUN_CKM_AES_ECB, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[7].me_threshold = kcf_aes_threshold; - (void) spl_strlcpy(kcf_cipher_mechs_tab[8].me_name, SUN_CKM_RC4, + (void) strlcpy(kcf_cipher_mechs_tab[8].me_name, SUN_CKM_RC4, CRYPTO_MAX_MECH_NAME); kcf_cipher_mechs_tab[8].me_threshold = kcf_rc4_threshold; /* 4 HMACs */ - (void) spl_strlcpy(kcf_mac_mechs_tab[0].me_name, SUN_CKM_MD5_HMAC, + (void) strlcpy(kcf_mac_mechs_tab[0].me_name, SUN_CKM_MD5_HMAC, CRYPTO_MAX_MECH_NAME); kcf_mac_mechs_tab[0].me_threshold = kcf_md5_threshold; - (void) spl_strlcpy(kcf_mac_mechs_tab[1].me_name, SUN_CKM_MD5_HMAC_GENERAL, + (void) strlcpy(kcf_mac_mechs_tab[1].me_name, SUN_CKM_MD5_HMAC_GENERAL, CRYPTO_MAX_MECH_NAME); kcf_mac_mechs_tab[1].me_threshold = kcf_md5_threshold; - (void) spl_strlcpy(kcf_mac_mechs_tab[2].me_name, SUN_CKM_SHA1_HMAC, + (void) strlcpy(kcf_mac_mechs_tab[2].me_name, SUN_CKM_SHA1_HMAC, CRYPTO_MAX_MECH_NAME); kcf_mac_mechs_tab[2].me_threshold = kcf_sha1_threshold; - (void) spl_strlcpy(kcf_mac_mechs_tab[3].me_name, SUN_CKM_SHA1_HMAC_GENERAL, + (void) strlcpy(kcf_mac_mechs_tab[3].me_name, SUN_CKM_SHA1_HMAC_GENERAL, CRYPTO_MAX_MECH_NAME); kcf_mac_mechs_tab[3].me_threshold = kcf_sha1_threshold; /* 1 random number generation pseudo mechanism */ - (void) spl_strlcpy(kcf_misc_mechs_tab[0].me_name, SUN_RANDOM, + (void) strlcpy(kcf_misc_mechs_tab[0].me_name, SUN_RANDOM, CRYPTO_MAX_MECH_NAME); kcf_mech_hash = mod_hash_create_strhash_nodtr("kcf mech2id hash", diff --git a/module/icp/spi/kcf_spi.c b/module/icp/spi/kcf_spi.c index edfb9765002c..96b9ea4130d5 100644 --- a/module/icp/spi/kcf_spi.c +++ b/module/icp/spi/kcf_spi.c @@ -606,7 +606,7 @@ init_prov_mechs(crypto_provider_info_t *info, kcf_provider_desc_t *desc) rand_mi = &desc->pd_mechanisms[mcount - 1]; bzero(rand_mi, sizeof (crypto_mech_info_t)); - (void) spl_strlcpy(rand_mi->cm_mech_name, SUN_RANDOM, + (void) strlcpy(rand_mi->cm_mech_name, SUN_RANDOM, CRYPTO_MAX_MECH_NAME); rand_mi->cm_func_group_mask = CRYPTO_FG_RANDOM; } else { diff --git a/module/os/windows/spl/spl-kmem.c b/module/os/windows/spl/spl-kmem.c index ba72a370e473..77c0d6ce7d99 100644 --- a/module/os/windows/spl/spl-kmem.c +++ b/module/os/windows/spl/spl-kmem.c @@ -3560,7 +3560,7 @@ kmem_cache_create( /* * Set cache properties. */ - (void) spl_strlcpy(cp->cache_name, name, KMEM_CACHE_NAMELEN + 1); + (void) strlcpy(cp->cache_name, name, KMEM_CACHE_NAMELEN + 1); strident_canon(cp->cache_name, KMEM_CACHE_NAMELEN + 1); cp->cache_bufsize = bufsize; cp->cache_align = align; diff --git a/module/os/windows/spl/spl-kstat.c b/module/os/windows/spl/spl-kstat.c index 61579cf812dd..dacda7fd5fcf 100644 --- a/module/os/windows/spl/spl-kstat.c +++ b/module/os/windows/spl/spl-kstat.c @@ -795,7 +795,7 @@ void kstat_set_string(char *dst, const char *src) { bzero(dst, KSTAT_STRLEN); - (void) spl_strlcpy(dst, src, KSTAT_STRLEN); + (void) strlcpy(dst, src, KSTAT_STRLEN); } void diff --git a/module/os/windows/spl/spl-taskq.c b/module/os/windows/spl/spl-taskq.c index a521ea40982a..556184be695e 100644 --- a/module/os/windows/spl/spl-taskq.c +++ b/module/os/windows/spl/spl-taskq.c @@ -2458,7 +2458,7 @@ taskq_create_common(const char *name, int instance, int nthreads, pri_t pri, * Make sure the name is 0-terminated, and conforms to the rules for * C indentifiers */ - (void) spl_strlcpy(tq->tq_name, name, TASKQ_NAMELEN + 1); + (void) strlcpy(tq->tq_name, name, TASKQ_NAMELEN + 1); strident_canon(tq->tq_name, TASKQ_NAMELEN + 1); tq->tq_flags = flags | TASKQ_CHANGING; diff --git a/module/os/windows/zfs/zfs_ctldir.c b/module/os/windows/zfs/zfs_ctldir.c index 488c5f30f87d..122487df9c49 100644 --- a/module/os/windows/zfs/zfs_ctldir.c +++ b/module/os/windows/zfs/zfs_ctldir.c @@ -995,8 +995,8 @@ zfsctl_snapshot_name(zfsvfs_t *zfsvfs, const char *snap_name, int len, if ((strlen(full_name) + 1 + strlen(snap_name)) >= len) return (SET_ERROR(ENAMETOOLONG)); - (void) spl_strlcat(full_name, "@", len); - (void) spl_strlcat(full_name, snap_name, len); + (void) strlcat(full_name, "@", len); + (void) strlcat(full_name, snap_name, len); return (0); } diff --git a/module/os/windows/zfs/zfs_ioctl_os.c b/module/os/windows/zfs/zfs_ioctl_os.c index 7744417138c4..37836a86d1de 100644 --- a/module/os/windows/zfs/zfs_ioctl_os.c +++ b/module/os/windows/zfs/zfs_ioctl_os.c @@ -185,7 +185,7 @@ NTSTATUS zpool_zfs_get_metrics(PDEVICE_OBJECT DeviceObject, PIRP Irp, PIO_STACK_ perf->zpool_allocated = pool_alloc; perf->zpool_size = pool_size; - spl_strlcpy(perf->zpoolHealthState, healthState, + strlcpy(perf->zpoolHealthState, healthState, sizeof (perf->zpoolHealthState)); } else diff --git a/module/os/windows/zfs/zfs_vnops_windows.c b/module/os/windows/zfs/zfs_vnops_windows.c index 9a24431b68cb..de7a6629f1d3 100644 --- a/module/os/windows/zfs/zfs_vnops_windows.c +++ b/module/os/windows/zfs/zfs_vnops_windows.c @@ -298,7 +298,7 @@ stream_parse(char *filename, char **streamname) // We now ADD ":$DATA" to the stream name. size_t remaining = PATH_MAX - (*streamname - filename); - if (spl_strlcat(*streamname, ":$DATA", remaining) >= remaining) + if (strlcat(*streamname, ":$DATA", remaining) >= remaining) return (SET_ERROR(ENAMETOOLONG)); return (0); diff --git a/module/zfs/dmu_send.c b/module/zfs/dmu_send.c index 31153df201cf..e2a3efe37ac0 100644 --- a/module/zfs/dmu_send.c +++ b/module/zfs/dmu_send.c @@ -3000,8 +3000,8 @@ dmu_send_estimate_fast(dsl_dataset_t *origds, dsl_dataset_t *fromds, char dsname[ZFS_MAX_DATASET_NAME_LEN + 6]; dsl_dataset_name(origds, dsname); - (void) spl_strlcat(dsname, "/", sizeof (dsname)); - (void) spl_strlcat(dsname, recv_clone_name, sizeof (dsname)); + (void) strlcat(dsname, "/", sizeof (dsname)); + (void) strlcat(dsname, recv_clone_name, sizeof (dsname)); err = dsl_dataset_hold(origds->ds_dir->dd_pool, dsname, FTAG, &ds); diff --git a/module/zfs/dsl_dir.c b/module/zfs/dsl_dir.c index 48b4970eb5f7..0a40175cd3cd 100644 --- a/module/zfs/dsl_dir.c +++ b/module/zfs/dsl_dir.c @@ -441,7 +441,7 @@ getcomponent(const char *path, char *component, const char **nextp) if (p - path >= ZFS_MAX_DATASET_NAME_LEN) return (SET_ERROR(ENAMETOOLONG)); - (void) spl_strlcpy(component, path, (p - path) + 1); + (void) strlcpy(component, path, (p - path) + 1); p++; } else if (p[0] == '@') { /* @@ -454,7 +454,7 @@ getcomponent(const char *path, char *component, const char **nextp) if (p - path >= ZFS_MAX_DATASET_NAME_LEN) return (SET_ERROR(ENAMETOOLONG)); - (void) spl_strlcpy(component, path, (p - path) + 1); + (void) strlcpy(component, path, (p - path) + 1); } else { panic("invalid p=%p", (void *)p); } diff --git a/module/zfs/dsl_prop.c b/module/zfs/dsl_prop.c index 4fb2be028b91..8f1605ec6129 100644 --- a/module/zfs/dsl_prop.c +++ b/module/zfs/dsl_prop.c @@ -65,7 +65,7 @@ dodefault(zfs_prop_t prop, int intsz, int numints, void *buf) * reaches userland through "zfs get", so restore the fill. */ bzero(buf, numints); - (void) spl_strlcpy(buf, zfs_prop_default_string(prop), + (void) strlcpy(buf, zfs_prop_default_string(prop), numints); } else { if (intsz != 8 || numints < 1) @@ -1036,7 +1036,7 @@ dsl_prop_get_all_impl(objset_t *mos, uint64_t propobj, if (flags & DSL_PROP_GET_LOCAL) continue; - (void) spl_strlcpy(buf, za.za_name, + (void) strlcpy(buf, za.za_name, (suffix - za.za_name) + 1); propname = buf; diff --git a/module/zfs/spa_misc.c b/module/zfs/spa_misc.c index 0e3063ab9297..a4c794b03582 100644 --- a/module/zfs/spa_misc.c +++ b/module/zfs/spa_misc.c @@ -1677,7 +1677,7 @@ spa_altroot(spa_t *spa, char *buf, size_t buflen) if (spa->spa_root == NULL) buf[0] = '\0'; else - (void) spl_strlcpy(buf, spa->spa_root, buflen); + (void) strlcpy(buf, spa->spa_root, buflen); } int diff --git a/module/zfs/zcp_get.c b/module/zfs/zcp_get.c index 1d334b7783bb..d6e5f1082c81 100644 --- a/module/zfs/zcp_get.c +++ b/module/zfs/zcp_get.c @@ -611,7 +611,7 @@ parse_userquota_prop(const char *prop_name, zfs_userquota_prop_t *type, */ int domain_len = strrchr(cp, '-') - cp; domain_val = kmem_alloc(domain_len + 1, KM_SLEEP); - (void) spl_strlcpy(domain_val, cp, domain_len + 1); + (void) strlcpy(domain_val, cp, domain_len + 1); cp += domain_len + 1; (void) ddi_strtoll(cp, &end, 10, (longlong_t *)rid); diff --git a/module/zfs/zfs_ioctl.c b/module/zfs/zfs_ioctl.c index c266aac80a62..fb92a33d58f2 100644 --- a/module/zfs/zfs_ioctl.c +++ b/module/zfs/zfs_ioctl.c @@ -745,7 +745,7 @@ zfs_get_parent(const char *datasetname, char *parent, int parentsize) /* * Remove the @bla or /bla from the end of the name to get the parent. */ - (void) spl_strlcpy(parent, datasetname, parentsize); + (void) strlcpy(parent, datasetname, parentsize); cp = strrchr(parent, '@'); if (cp != NULL) { cp[0] = '\0'; diff --git a/module/zfs/zio_inject.c b/module/zfs/zio_inject.c index aacff6a6142f..6e4a97a1b5bd 100644 --- a/module/zfs/zio_inject.c +++ b/module/zfs/zio_inject.c @@ -894,7 +894,7 @@ zio_inject_list_next(int *id, char *name, size_t buflen, if (handler) { *record = handler->zi_record; *id = handler->zi_id; - (void) spl_strlcpy(name, spa_name(handler->zi_spa), buflen); + (void) strlcpy(name, spa_name(handler->zi_spa), buflen); ret = 0; } else { ret = SET_ERROR(ENOENT);