The Nuvoton NPCX eSPI driver parses Target-Attached-Flash (TAF) requests in espitafparse() (drivers/espi/espinpcx.c), taking the 12-bit length field directly out of the request header in the FLASHRXBUF FIFO. For write and RPMC OP1 requests it copied DIVROUNDUP(len, 4) words into struct espitafpckt.src, a fixed 16-word (64-byte) member of a static structure, without ever comparing the length against that buffer or against the negotiated eSPI receive payload size (rx-plsize, 64 bytes on all in-tree NPCX/NPCK SoCs). Both the loop counter and the word count were uint8t, so the copy ran up to 255 iterations.
An eSPI controller that emits a flash request whose header claims a payload larger than the negotiated maximum therefore drives up to 239 32-bit stores past the end of the structure — roughly 956 bytes of out-of-bounds write into .bss — from the eSPI flash receive interrupt handler, while the source reads walk past the end of the 18-word FLASHRXBUF register array into neighbouring eSPI registers. The same unvalidated length was then passed on to flashwrite() in espitafnpcxflashwrite() with the 64-byte src buffer as source, reading up to 4 KB of adjacent controller SRAM into SPI flash at a host-chosen address, where the host can read it back. A related fixed-size stack buffer in tafnpcxcompletionhandler() was overflowed by four bytes on the maximum-size completion path on SoCs whose tx-plsize is 64.
The defect is reachable only from the eSPI host side, and only by a request that violates the negotiated payload size, so it requires host firmware or bus-level control rather than ordinary host software; a compliant maximum-length write fits the buffer exactly and is handled correctly. Because TAF exists specifically so that the embedded controller arbitrates host access to shared flash (espitafcheckreadprotect() / espitafcheckwriteprotect()), corrupting controller memory from a host-supplied field crosses the trust boundary the feature asserts; consequences range from controller crash or hang to corruption of driver state that gates the flash protection regions.
The fix widens the counters, rejects any request whose word count exceeds both ARRAYSIZE(FLASHRXBUF) - NPCXESPIFLASHHEADERADDRESSLEN and NPCXTAFRXPAYLOADWORDS (derived from the devicetree rx-plsize), marks such packets with a new invalidflag so espitafwork() answers them with an unsuccessful completion instead of processing them, bounds the transmit payload against FLASHTXBUF, and adds a default case for unsupported cycle types. Only configurations with CONFIGESPITAF enabled on an NPCX/NPCK target are affected; no in-tree board enables it by default.
The kernel helper zwaitqhead() (kernel/include/waitq.h) reads the head of a thread wait queue, but on SMP systems it was called from several places without holding the scheduler spinlock schedspinlock that serializes wait-queue mutation. The consequential call site is ztimerexpirationhandler() in kernel/timer.c, which held only the timer subsystem lock timerlock while reading timer->waitq and then unpending, setting the return value of, and readying the thread it obtained. haltthread() in kernel/sched.c removes an aborted thread from its wait queue while holding only schedspinlock, so the two paths are not mutually exclusive.
When the last waiter is removed on another CPU inside that window, sysdlistpeekhead() can return the list sentinel — a pointer into the ktimer object itself — typed as a struct kthread (with CONFIGWAITQSCALABLE, rbgetmin() can return an arbitrary node of a tree being restructured), or it can return a pointer to a thread that has already been unpended and, for dynamically allocated threads, freed. The handler then calls unpendthreadnotimeout(), which performs a sysdlistremove() through pointers read from that object, and archthreadreturnvalueset(), which writes unconditionally through the pointer.
The result is out-of-bounds and use-after-free writes in supervisor context, plus scheduler state corruption; a milder outcome is a thread that has re-pended elsewhere being woken with a success return value from an unrelated blocking call. All of the primitives needed to drive the interleaving — ktimerstart(), ktimerstatussync(), kthreadcreate() and kthreadabort() — are system calls, so with CONFIGUSERSPACE enabled an unprivileged user-mode thread can attempt the race against kernel-owned data.
Exploitation requires a multi-CPU build (CONFIGMPMAXNUMCPUS greater than one; the affected region is a no-op on uniprocessor targets), local code execution, and winning a narrow timing window, so the practical outcome is most often a kernel crash rather than controlled corruption. The fix splits the helper into zwaitqheadlocked() for callers that already hold the scheduler spinlock and a zwaitqhead() that acquires it, and brings the timer expiration handler's head read, unpend and ready under a single LOCKSCHEDSPINLOCK region.
The Bluetooth Classic A2DP host stores a codec Information Element (IE) in struct bta2dpcodecie, a one-byte len field plus a fixed 8-byte codecie[] array. When an AVDTP peer sent a codec IE longer than 8 bytes, a2dpprocessconfigind() in subsys/bluetooth/host/classic/a2dp.c copied only the first 8 bytes but assigned the full, attacker-chosen length to len, and then persisted that inconsistent structure into the application's struct bta2dpstream via stream->codecconfig. bta2dpgetconfigcb() and bta2dpsetconfigcb() had the same truncate-the-copy-but-keep-the-length pattern.
Every later consumer trusts len. In particular the responder for an incoming AVDTP GETCONFIGURATION, a2dpgetconfigind(), executes netbufaddmem(rspbuf, &ep->stream->codecconfig.codecie[0], ep->stream->codecconfig.len), so a paired BR/EDR peer that first sends a SETCONFIGURATION carrying an oversized codec IE and then requests the configuration back causes the stack to read up to roughly 245 bytes past the end of the 8-byte array and transmit them in the response. codecconfig is the last member of struct bta2dpstream, so the read runs into memory adjacent to the application's stream object. The same inconsistency is also handed to the configreq(), reconfigreq() and getconfigrsp() application callbacks, where a natural memcpy() of len bytes performs a further out-of-bounds read.
Reaching the on-air disclosure requires CONFIGBTCLASSIC with CONFIGBTA2DP, an AVDTP peer that has completed pairing (the AVDTP L2CAP channel requires BTSECURITYL2), and a local endpoint registered for a non-SBC codec — BTA2DPMPEG1/BTA2DPMPEG2/BTA2DPATRAC/BTA2DPVENDOR via BTA2DPEPINIT() — because the SBC branch already pins the IE length to 4 bytes. The bta2dpgetconfigcb() response path has no such gate and hands the lying length to any application. There is no out-of-bounds write: the netbuf tailroom check bounds the destination, so the impact is limited to disclosure of a bounded region of adjacent RAM to the peer.
The fix replaces the hardcoded BTA2DPMAXIELENGTH with the configurable CONFIGBTA2DPCODECMAXIELEN (default 8, range 4–253), sizes codecie[] from it, and rejects any received IE that exceeds it instead of silently truncating, so len can no longer exceed the number of valid bytes.
The user-mode verifier for the pecitransfer() system call, zvrfypecitransfer() in drivers/peci/pecihandlers.c, checked only the device object permission and copied the caller-supplied struct pecimsg with kusermodefromcopy(). That copy validates the outer structure only; the buf pointers and len values inside the embedded struct pecibuf members txbuffer and rxbuffer remained fully caller-controlled and were passed unchecked to the driver.
Every in-tree PECI driver dereferences those pointers in supervisor mode: pecinpcxtransfer() in drivers/peci/pecinpcx.c and peciit8xxx2transfer() in drivers/peci/peciiteit8xxx2.c copy hardware bytes into rxbuffer.buf[i] and read command bytes out of txbuffer.buf[i], and pecixecread()/pecixecwrite() in drivers/peci/pecimchpxec.c do the same with no bound on rxbuffer.len at all. A user-mode thread that has been granted the PECI device object — the normal way an application exposes a device under CONFIGUSERSPACE — could therefore point rxbuffer.buf at kernel data structures and have the driver write hardware-supplied bytes there, or point txbuffer.buf at kernel memory and have its contents pushed onto the PECI/eSPI out-of-band bus.
The impact is a full user-mode-to-kernel escape: an arbitrary kernel write primitive (integrity and, through it, code execution at supervisor privilege), an arbitrary kernel read disclosed to a PECI bus observer, and a trivially reachable supervisor-mode fault if an unmapped address is supplied. Exploitation requires CONFIGUSERSPACE=y, one of the PECI drivers enabled (CONFIGPECINPCX, CONFIGPECIXEC or CONFIGPECIITEIT8XXX2), and local code execution in a user-mode thread holding the PECI device.
The fix adds KSYSCALLMEMORYREAD() and KSYSCALLMEMORYWRITE() checks over the two embedded buffers, with a sizeaddoverflow() guard on the frame-check-sequence byte accounted for in the RX length.
The Bluetooth Classic Hands-Free Profile HF role in subsys/bluetooth/host/classic/hfphf.c does not release the AT command buffers that are still queued on hf->txpending when the RFCOMM DLC goes down. hfphfdisconnected() only cancelled the work items and cleared hf->acl, and no other code path ever dequeues those buffers, so each one is permanently lost from the fixed hfpool (CONFIGBTMAXCONN + 1 buffers, i.e. two in the default configuration). The same function also left BTHFPHFFLAGCONNECTED set, and hfphfcreate() wiped the object with memset() as soon as hf->acl was NULL, without checking whether hf->work, hf->slcwork or hf->deferredwork was still pending or running.
The remote Audio Gateway peer controls the trigger end to end and needs no application involvement: an unsolicited +BCS: 1 response makes bcshandle() call bthfphfselectcodec() and queue an AT+BCS buffer, which cannot be transmitted while BTHFPHFFLAGRXONGOING is set, and an immediately following RFCOMM DISC strands that buffer. Repeating the connect / +BCS / disconnect cycle a couple of times empties hfpool for good.
With the pool exhausted, HFP command sending fails permanently, and because hfphfsendcmd() allocates with KFOREVER (btrfcommcreatepdu() → btconncreatepdu()) from the Bluetooth RX context — where that timeout is not downgraded to KNOWAIT — one further +BCS from the peer parks the Bluetooth RX thread inside netbufalloc() with no possible waker, halting all HCI event processing until the device is rebooted. Separately, reusing the HF slot while a work handler was still queued or running allowed memset() to wipe live kwork items out from under the system workqueue, corrupting shared kernel workqueue state. Exploitation requires an established HFP session (for SSP devices L2CAP enforces authentication before the RFCOMM channel is reachable) and a build with CONFIGBTHFPHF enabled.
The user-mode syscall verifier zvrfyrtiocqegetmempoolbuffer() in subsys/rtio/rtiosyscalls.c validated its two output parameters, buff and bufflen, with KSYSCALLMEMORYREAD() rather than KSYSCALLMEMORYWRITE(). Both are pure out-parameters: zimplrtiocqegetmempoolbuffer() stores the mempool buffer address and its length through them. The distinction is enforced by every architecture validator — archbuffervalidate() on RISC-V PMP explicitly accepts the global romregion/rodataregion for reads only, and the ARM MPU validators accept PROURO and PRWURO regions for reads only — so a read-validated pointer is not necessarily writable by the calling thread.
An unprivileged user-mode thread that has been granted access to an RTIO context (rtioaccessgrant()) can invoke the rtiocqegetmempoolbuffer() syscall with buff/bufflen pointing at memory it may read but not write, and with a completion-queue entry whose flags field it fully controls (only RTIOCQEFLAGMEMPOOLBUFFER in flags[7:0] is required; flags[19:8] and flags[31:20] supply the block index and count). The kernel then performs a pointer-sized and a 4-byte store through those addresses while running in supervisor mode. Reaching the store requires CONFIGUSERSPACE together with CONFIGRTIOSYSMEMBLOCKS, the configuration behind RTIODEFINEWITHMEMPOOL; without the latter the implementation returns -ENOTSUP and writes nothing.
Two outcomes follow. If the target lies in a memory-domain partition declared KMEMPARTITIONPRWURO (privileged read-write, user read-only), the kernel-mode store succeeds and deposits partly attacker-chosen values into memory the memory domain had marked read-only for that thread, bypassing the partition's write protection. If the target lies in globally readable but unwritable memory such as .text/.rodata in flash, the store faults in kernel mode; the fault is delivered to zfatalerror() and the default ksysfatalerrorhandler() halts or reboots the device, giving an unprivileged thread a reliable full denial of service.
The read-permission check does bound the damage: the address must still be readable by the calling thread, so kernel-private memory cannot be targeted and no confidential data is disclosed. The fix replaces both checks with KSYSCALLMEMORYWRITE(), so a thread lacking write permission is terminated by KOOPS before the implementation runs; the cqe argument correctly remains read-validated.
The CC1200 sub-GHz IEEE 802.15.4 driver takes the PHY packet length from the first byte of the radio's RX FIFO in getpacketlength() and validates it in verifyrxfifovalidity() (drivers/ieee802154/ieee802154cc1200.c). That validation only enforced a lower bound (pktlen < 3); its second clause rejects a FIFO that holds more bytes than declared and therefore never bounds large values. Because the driver configures the radio for variable packet length with PKTLEN = 0xFF, the length byte is an over-the-air value of 0-255 that reached netpktrxallocwithbuffer() and the subsequent SPI burst read unclamped.
An unauthenticated attacker within radio range can transmit a frame that presents a length byte above the IEEE 802.15.4 maximum of 127 while the chip's 128-byte RX FIFO has not overflowed, so the driver's RXFIFOERROR pre-check does not fire. Reaching this state depends on RF framing and timing, since the RX thread is driven off the PKTSYNCRXTX signal and may sample the FIFO before a packet is complete; no pairing, authentication, or user interaction is required.
With the default CONFIGNETBUFFIXEDDATASIZE and CONFIGNETBUFDATASIZE=128, an allocation larger than one fragment is satisfied by a chain of fragments, but readrxfifocontent() performs a single SPI burst of pktlen bytes into the first fragment's buf->data and then calls netbufadd() with the same length. A length above 128 therefore writes up to 127 bytes past the end of a fragment in the contiguous rxbufs pool and leaves a netbuf whose len exceeds its capacity. The overflowing bytes are FIFO-underflow output rather than attacker-chosen data, so the practical impact is network-buffer-pool corruption and crashes rather than a controlled write. Only builds that enable CONFIGIEEE802154CC1200 with an attached CC1200 radio are affected.
The AVDTP signalling layer arms a 6-second command timeout (AVDTPTIMEOUT) in avdtpsendcmd() whenever the local device issues an AVDTP request. Every response handler in subsys/bluetooth/host/classic/avdtp.c cancels session->timeoutwork before clearing the pending request, but btavdtpl2capdisconnected() only cleared session->req and left the delayable work armed. The struct btavdtp session is the first member of struct bta2dp, whose instances live in a static per-connection array, and a2dpgetconnection() in subsys/bluetooth/host/classic/a2dp.c wipes a slot with memset() as soon as session->brchan.chan.conn is NULL — which the L2CAP layer arranges immediately after the disconnect callback returns.
A remote Bluetooth BR/EDR peer triggers the condition by opening the AVDTP signalling channel, letting the local application issue an AVDTP procedure (the standard A2DP flow issues bta2dpdiscover() from the connected callback), then tearing the signalling channel down without responding and re-opening it within the remaining timeout window. The reconnect reaches a2dpaccept() → a2dpgetconnection(), which zeroes the whole struct bta2dp while the work item's struct timeout is still linked into the kernel's global timeout list. Both the disconnect and the reconnect instants are chosen by the attacker, so no race has to be won; an encrypted BR/EDR link is required, and CONFIGBTA2DP (experimental) must be enabled.
With the default CONFIGTIMEOUTBACKENDDLIST backend the kernel decides a timeout is inactive by testing whether its list node is linked, so after the memset() the kernel treats a node that is still on the global timeout list as free: the announce path pops a node whose links and handler pointer are NULL, and the next btworkreschedule() on the recycled session re-inserts the same node into a list that already contains it. The consequence is corruption of the single kernel data structure every timer, timeslice and sleep depends on — writes through zeroed or stale list pointers and a NULL handler dispatch — producing a system-wide crash or hang rather than a fault confined to the A2DP session.
The fix cancels session->timeoutwork at the top of btavdtpl2capdisconnected() and moves kworkinitdelayable() from avdtpsendcmd() to the session-establishment paths btavdtpconnect() and btavdtpl2capaccept(), so the work item is always initialized before a disconnect callback can run.
The Bluetooth Mesh Access layer delayable-message machinery in subsys/bluetooth/mesh/delayablemsg.c manages a static pool of CONFIGBTMESHACCESSDELAYABLEMSGCOUNT message contexts (default 4) threaded onto a freectx or busyctx list. releasedelayablemsgctx() returned a context to freectx only when sysslistfindandremove() found it on busyctx. The chunk-exhaustion error path in btmeshdelayablemsgmanage() releases a context that was already popped off freectx but not yet inserted on busyctx, so the context was appended to neither list and was permanently lost from the pool. Because btmeshdelayablemsginit() runs only once from btmeshinit(), and node reset/suspend only drain busyctx, the loss survives unprovision, re-provision and suspend/resume — it is cleared only by a reboot.
The error path is reached when the chunk pool (CONFIGBTMESHACCESSDELAYABLEMSGCHUNKCOUNT, default 40 chunks) is short of the chunks a new delayed message needs and the oldest pending message cannot be flushed because btmeshaccesssend() returns -EBUSY or -ENOBUFS (all segmented-TX contexts busy, or advertising/segment buffers exhausted). A peer that is provisioned into the mesh network — holding a valid NetKey plus an AppKey or DevKey bound to a server model — can drive both conditions by flooding the victim with requests that elicit segmented responses while withholding the Segment Acknowledgment, which keeps the transmit contexts occupied while queued responses hold the chunk pool. Each further request that arrives in that window leaks one context.
Once the pool is drained, btmeshdelayablemsgmanage() returns -ENOMEM for every subsequent send and btmeshmodelsend() propagates that error with no fallback to an immediate transmit. Since CONFIGBTMESHACCESSDELAYABLEMSG and CONFIGBTMESHACCESSDELAYABLEMSGCTXENABLED are both enabled by default, ctx->rnddelay is set for every received access message, so all replies from Configuration Server, Health Server and application server models are silently dropped. The node keeps receiving and processing traffic but never answers, a persistent denial of service until the device is restarted. There is no memory corruption, information disclosure or integrity impact; releases before the fix compute the chunk count from the transmit buffer capacity rather than its payload length, making chunk-pool starvation — and therefore the leak — considerably easier to provoke.
The Bluetooth Mesh Access-layer delayable message queue in subsys/bluetooth/mesh/delayablemsg.c shares three intrusive lists (busyctx, freectx, freechunks) and one kworkdelayable between two cooperative threads with no synchronization: btmeshdelayablemsgmanage() runs on the Bluetooth host RX thread, while delayablemsghandler() runs on the system workqueue. Both block while the lists are in an intermediate state — the enqueuer in btrand(), both in btmeshaccesssend() — which lets the other context observe torn state. delayablemsghandler() used the result of sysslistget() on busyctx without a NULL check, and allocatedelayablemsgctx() tested sysslistisempty() and then assumed a separate sysslistget() would succeed.
Because CONFIGBTMESHACCESSDELAYABLEMSG and CONFIGBTMESHACCESSDELAYABLEMSGCTXENABLED default to y, every model response to a received mesh message is parked on this queue, so the enqueuer runs as a direct consequence of inbound traffic. A peer that can get access-layer messages accepted by a model (it must hold the NetKey and a bound AppKey, so a legitimate or compromised network member) controls the arrival rate, the size of the elicited responses and the destination address, and thereby controls occupancy of the 4-entry context pool and the 40-entry chunk pool. Exhausting the chunk pool forces btmeshdelayablemsgmanage() down the purge path, which sends and releases the only queued message and leaves busyctx empty while the delayable work item is still armed; if the enqueuer then yields, the handler runs against the empty list.
The result is a read-modify-write through a NULL-based pointer (pendingmsg->firedtime += 10) at a fixed low address on the system workqueue thread. On typical targets that faults, aborting the system workqueue thread — which stops all workqueue-driven functionality, including the mesh timing machinery — or resetting the node, so the impact is loss of availability of the affected node. The write value and address are fixed and no attacker-controlled length or index is involved, so this is not exploitable for memory corruption or code execution. The same unsynchronized window also allowed one message context to be selected and released by two contexts at once, producing duplicated transmissions, duplicated send-completion callbacks and crossed chunk ownership.
The fix serializes every list mutation with a kspinlock that is never held across a blocking call, makes pushmsgfromdelayablemsgs() detach the queue head atomically so a message being sent is on no list, removes the unchecked sysslistget() from delayablemsghandler(), and drops any message that reaches the front of the queue while the stack is suspended or reset.
The Bluetooth Link Layer control procedure code in subsys/bluetooth/controller/llsw/ullllcpconnupd.c retains the received RX node while a Connection Update / Connection Parameter procedure waits for its instant, so the node can later carry the host notification (llcprxnoderetain(), which marks it NODERXTYPERETAIN and thereby suppresses the normal recycling in ull.c). The default: arm of llcprpcurx() and llcplpcurx() — the "invalid PDU, terminate the connection" path — completed the procedure without releasing that retained node. llcprrcheckdone() then dequeued and freed the procedure context with ctx->noderef.rx still pointing at the retained node, dropping the last reference to it.
A peer device in radio range can reach this without pairing, bonding, or encryption. Against a peripheral, the peer sends a well-formed LLCONNECTIONUPDATEIND with an instant a few connection events in the future (the node is retained and the procedure enters RPCUSTATEWAITINSTANT), then, before the instant is reached, sends any other LL Control PDU such as LLLENGTHREQ. ullcprx() routes that PDU into the active remote Connection Update procedure, which takes the invalid-PDU path. The central role is reachable symmetrically after accepting an LLCONNECTIONPARAMREQ, and the local-procedure variant is reachable with LLREJECTIND.
With CONFIGBTCTLRASSERTDEBUG enabled (its default), the resulting state violates the invariant asserted in llcprrcheckdone(), so the two-PDU sequence produces an immediate fatal error in the controller. With those asserts disabled, each occurrence permanently loses one node from the controller's small fixed RX pool (sized from CONFIGBTCTLRRXBUFFERS, which defaults to 1); repeating the sequence across reconnections exhausts the pool, after which the link-layer receive path operates on a NULL node. The impact is an unauthenticated, remotely triggerable denial of service persisting until reboot; there is no memory-disclosure or memory-corruption consequence, since the leaked node simply becomes unreachable.
The RFC 6528 initial-sequence-number implementation in subsys/net/ip/tcp.c derived every TCP ISN from SHA-256(uniquekey || four-tuple) plus a uptime-derived offset, where uniquekey is a 128-bit secret filled once by syscsrandget(). The return value of that call was discarded and the once guard was latched to true even when the call failed. Because syscsrandget() leaves the destination buffer untouched on failure (it deliberately propagates the entropy-driver error rather than filling the buffer), a single failed call left uniquekey as its all-zero BSS content for the remainder of the boot, with no retry, no log message and no fallback.
A failure of the cryptographic random source is required to reach the weak state — for example -ENODEV/-EIO from the entropy driver in subsys/random/randomentropydevice.c (the in-tree comment notes that the hardware RNG "might still be gathering entropy during early boot situations"), or psageneraterandom() failing in subsys/random/randompsa.c when PSA crypto is not initialised or is backed by a Bluetooth-HCI entropy device that is not yet up. The first TCP connection is what triggers key generation, so a remote peer that reaches a listening port immediately after boot, or that can provoke a reboot, has indirect influence over whether generation coincides with that window. tcpinitisn() is called for every passive open in tcpconnnew() and every active open in nettcpconnect(), so the poisoned key governs all TCP connections for that boot.
With an all-zero key the ISN becomes a public function of the connection four-tuple plus a device-wide time offset. An off-path attacker can compute the hash term offline for any four-tuple and recover the shared time offset from a single observed ISN, after which the ISN the device will pick for other four-tuples is predictable. That defeats exactly the protection RFC 6528 provides: blind TCP connection spoofing against peers that trust the source address, and blind data injection into or reset of connections whose four-tuple can be guessed. Devices whose entropy source never errors were never in the weak state.
The fix moves key generation into a single guarded helper that latches only on success, logs the error otherwise, and makes tcpinitisn() fall back to sysrand32get() — the behaviour already used when CONFIGNETTCPISNRFC6528 is disabled — instead of hashing with a known-constant key.
The user-mode syscall verifiers zvrfyfuelgaugegetprops() and zvrfyfuelgaugesetprops() in drivers/fuelgauge/fuelgaugesyscallhandlers.c declared two variable-length arrays, union fuelgaugepropval kvals[len] and fuelgaugepropt kprops[len], sized directly by the caller-supplied len argument. len is an unvalidated sizet taken straight from the syscall ABI, and the VLAs were allocated before any check at all — including before the KSYSCALLDRIVERFUELGAUGE() object-permission check. The subsequent kusermodefromcopy() calls validated only that the user source buffer was readable; the kernel destination was never bounds-checked, since it was sized by the same attacker-chosen len.
Any thread running in user mode with CONFIGUSERSPACE enabled can invoke fuelgaugegetprops() or fuelgaugesetprops() with a large len. This first displaces the supervisor stack pointer by an arbitrary attacker-chosen amount — Zephyr does not build with stack-clash probing, so the displacement itself does not fault, and the nested calls made by the verifier then write frames below the privileged stack. If the caller has been granted access to a fuel-gauge device object, the memcpy inside kusermodefromcopy() additionally writes len sizeof(union fuelgaugepropval) bytes of fully attacker-controlled data starting well below the stack base. CONFIGPRIVILEGEDSTACKSIZE defaults to 1024 bytes, so a len of roughly 170 already exhausts it.
The result is an out-of-bounds write in supervisor mode with attacker-controlled length and, on the permitted path, attacker-controlled content — a break out of the user-mode sandbox into kernel memory, leading to kernel code execution or a system crash. A stack guard region does not contain it, because the copy begins below the guard and walks upward, corrupting unprotected memory before the guard is reached. The fix removes the kernel-side copies entirely and validates the caller's arrays in place with KSYSCALLMEMORYARRAYREAD() / KSYSCALLMEMORYARRAYWRITE(), which also handle the len size multiplication overflow; this is safe because neither fuelgaugepropt nor union fuelgaugepropval contains embedded pointers.
The user-mode verification handler for the devicedeinit() system call, zvrfydevicedeinit() in kernel/device.c, validated its dev argument with KSYSCALLOBJINIT(dev, KOBJANY). kobjectvalidate() short-circuits its type comparison when the requested type is KOBJANY, so the check reduced to "this pointer is the base address of some kernel object the calling thread has been granted" — the object's actual type was never compared, and KSYSCALLOBJINIT also skips the initialization-state check. The sibling handlers zvrfydeviceinit() and zvrfydeviceisready() already used KOBJDRIVERANY and were unaffected.
A thread running in user mode can therefore pass any kernel object it holds permission on — most usefully a thread stack object obtained from the kthreadstackalloc() syscall or a statically defined KTHREADSTACK it was granted in order to spawn a child user thread — whose backing memory is writable from user mode. zimpldevicedeinit() then interprets those attacker-written bytes as a struct device: it dereferences the state pointer read out of the object, calls the function pointer read out of ops.deinit, and on success writes through state again. The result is an indirect call to an arbitrary address executed in supervisor mode, plus an arbitrary kernel read and a single-byte kernel write.
Exploitation gives a local unprivileged thread full kernel code execution, defeating the CONFIGUSERSPACE isolation boundary entirely; a less precise attempt yields a supervisor-mode fault and a system crash. The defect is only reachable in builds that enable both CONFIGUSERSPACE and CONFIGDEVICEDEINITSUPPORT — with de-initialization support disabled, zimpldevicedeinit() returns -ENOTSUP without ever dereferencing the pointer. In v4.2.x and v4.3.x, CONFIGDEVICEDEINITSUPPORT defaulted to y, so every CONFIGUSERSPACE build of those releases is exposed unless the option was explicitly turned off. From v4.4.0 the option is opt-in (no default, and not selected by any in-tree subsystem), so a v4.4.x build is exposed only if it enables the option explicitly. The v4.2 line is no longer maintained and receives no backport.
The fix changes the object check to KOBJDRIVERANY, which constrains the argument to the build-generated driver object type range (KOBJDRIVERFIRST..KOBJDRIVERLAST) — the real struct device instances placed by the linker — so the state and ops.deinit fields are once again kernel-controlled.
The ARM64 MMU back-end allocated address space identifiers (ASIDs) for memory domains with a bare round-robin counter in archmemdomaininit() (arch/arm64/core/mmu.c). VMASIDBITS is 8, so only 255 ASIDs exist; once the counter wrapped, archmemdomaininit() could hand an ASID to a new domain while a still-live domain held the same one. Domain-private mappings are installed non-global (MTNG), so the ASID is the only tag separating one domain's cached translations from another's in the TLB.
The context-switch path in zarm64swapptables() only flushes the TLB when the outgoing and incoming domains carry the same ASID, which does not cover a duplicate reached through a third domain: for domains A and C sharing an ASID and an unrelated domain B, the schedule A -> B -> C never takes the flush branch, so the ASID-tagged entries A populated remain resident while C runs. Under SMP two live domains sharing an ASID can additionally be resident on two CPUs at once, which the architecture does not allow for distinct translation-table sets.
Triggering the wrap requires a CONFIGUSERSPACE application on ARM64 that creates more than 255 memory domains over its lifetime; kmemdomaininit() and kmemdomaindeinit() are supervisor-only APIs and are not exposed as syscalls, so an unprivileged thread cannot drive the counter directly. Once two live domains alias, however, a user-mode thread in one domain can read and write memory belonging to the other domain's partitions and thread stacks with that domain's permissions, defeating the memory-domain isolation boundary.
The fix scans the live domainlist before assigning an ASID, advances the round-robin counter past ASIDs already in use, and returns -ENOMEM when all are taken, so domain creation fails closed instead of silently aliasing.
The ITE IT8xxx2 SHI host-command backend (subsys/mgmt/echostcmd/backends/echostcmdbackendshiite.c) copied the 8-byte host-command request header from the SPI Rx FIFO directly into the shared receive buffer data->inmsg and only afterwards checked the protocol version and the derived packet length. The interrupt handler also accepted a chip-select assertion and an Rx-valid-length (RVLI) interrupt in any driver state other than SHISTATEDISABLED, so a new header could be parsed while the host-command thread was still processing the previous request out of the very same buffer.
The host processor is the SPI controller and drives both chip select and the clock. After sending a well-formed request it can immediately de-assert chip select — which returns the driver to the ready state and re-enables the FIFO — and start a second transaction carrying a header with datalen = 0xFFFF. Those eight bytes are written into inmsg before the oversized length is rejected, so they land in a buffer whose contents verifyrx() in subsys/mgmt/echostcmd/echostcmdhandler.c has already validated. If this lands in the window before the host-command thread executes args.inputbufsize = rxheader->datalen, the framework hands the registered command handler a 65535-byte input length over a 256-byte buffer.
The result is an out-of-bounds read of up to roughly 64 KiB beyond the request buffer: command handlers that copy or echo inputbufsize bytes disclose adjacent embedded-controller memory back to the host or overflow the response buffer, and a read past the end of SRAM faults the controller. The same race also allows cmdid and cmdver to be swapped after checksum verification and after handler lookup. Exploitation requires the ability to drive the inter-processor SHI bus (a compromised host OS or physical access to the SPI lines) and winning a timing race, which the SPI controller can retry indefinitely.
The fix parses the header into a local struct echostcmdrequestheader and copies it into inmsg only after the length has been bounded by sizeof(data->inmsg), and ignores chip-select and RVLI interrupts outside SHISTATEREADYTORECV/SHISTATERECEIVING. A residual, bounded race remains: an end-of-transaction interrupt still resets the state to ready while the host-command thread owns the buffer, so a valid second request can still overwrite the in-flight request's contents, unlike the NPCX backend which parks in SHISTATECNLRESPNOTRDY while the buffer is in use.
The LE Audio Broadcast Sink in subsys/bluetooth/audio/bapbroadcastsink.c copies subgroup metadata from a received Basic Audio Announcement (BASE) into the static Broadcast Audio Scan Service parameter structure modsrcparam without any bounds check. In basesubgroupmetacb() the destination element was selected as modsrcparam.subgroups[modsrcparam.numsubgroups] with no test against ARRAYSIZE(modsrcparam.subgroups) (sized by CONFIGBTBAPBASSMAXSUBGROUPS, default 1), and the metadata was copied with memcpy() using the raw on-air length returned by btbapbasegetsubgroupcodecmeta() into a metadata array sized by CONFIGBTAUDIOCODECCFGMAXMETADATASIZE (default 4). The BASE validator btbapbasegetbasefromad() only checks structural consistency and permits up to ~24 subgroups and metadata LTVs of ~240 octets.
The defect is reached from the periodic advertising receive callback: parecv() → btdataparse() → padecodebase() → updaterecvstatebase() → btbapbaseforeachsubgroup() → basesubgroupmetacb(). Every broadcast sink registers a scan-delegator receive state at creation (btbapbroadcastsinkcreate() calls broadcastsinkaddsrc()), and CONFIGBTBAPBROADCASTSINK depends on CONFIGBTBAPSCANDELEGATOR, so the path is active in every broadcast-sink build once the device is periodic-advertising-synced. An attacker in radio range who operates a broadcast source the device syncs to — or who impersonates the advertiser address and SID of one already in use, periodic advertising data being unauthenticated — can change the BASE at will; each new BASE is re-parsed.
A crafted BASE therefore writes attacker-chosen bytes past the end of a fixed static object in .bss: up to roughly 236 bytes for an oversized metadata LTV, plus whole struct btbapbasssubgroup records for each subgroup beyond CONFIGBTBAPBASSMAXSUBGROUPS. This is memory corruption of adjacent Bluetooth-audio state reachable with no pairing, bonding or GATT connection, with a potential for remote code execution in the Bluetooth RX thread; in addition, the unvalidated metadatalen is forwarded to btbapscandelegatormodsrc(), which neither clamps it nor rejects it, leading to a further copy into the receive state and to out-of-bounds memory being disclosed in the BASS receive-state notification sent to a connected Broadcast Assistant.
The fix rejects a BASE carrying more subgroups than the receive state can hold (discarding the update entirely) and omits metadata that does not fit rather than copying it, and additionally honours the previously-ignored error return of the subgroup decode pass.
The NXP GAU ADC driver (drivers/adc/adcmcuxgauadc.c) validated the caller-supplied sequence->buffersize, which is expressed in bytes, against the number of active channels, which is a sample count. It then stored that byte count directly in data->resultslength and used it in mcuxgauadcreadsamples() as the number of uint16t slots available. Because each conversion result occupies sizeof(uint16t) bytes, a buffer that was accepted as "large enough" could be written with up to twice its size in bytes, so every sample past the buffer's midpoint was written out of bounds.
adcread() and adcreadasync() are Zephyr system calls. The syscall verifier in drivers/adc/adchandlers.c only confirms that the caller owns buffersize writable bytes (KSYSCALLMEMORYWRITE); deciding whether that size is sufficient for the requested channels and extrasamplings is delegated entirely to the driver. On a build with CONFIGUSERSPACE=y, a user-mode thread that has been granted the ADC device object could therefore submit a deliberately half-sized buffer and cause the driver's work-queue handler — which runs in supervisor mode, outside the caller's MPU restrictions — to write ADC conversion results past the end of that buffer, at an address and for a length of the caller's choosing.
The overrun is bounded by the requested sequence: with sequence->options->extrasamplings set, the sampling loop walks the buffer pointer forward across every sampling, so the total overrun can reach the full size of the supplied buffer (kilobytes for a large extrasamplings). The written words are 16-bit ADC conversion results, so the content is only partially attacker-influenced (via the selected analog input, gain and resolution), but the destination and length are fully controlled — sufficient for kernel memory corruption, a crash, or a userspace-to-kernel privilege escalation. Builds without CONFIGUSERSPACE, or on SoCs other than NXP RW61x with the GAU ADC node enabled, are not exposed to the privilege boundary; there the same defect only causes a silent overflow when the application itself passes an undersized buffer.
The fix replaces the ad-hoc check with the shared adcsequencevalidatebuffer() helper (validating against numchannels sizeof(uint16t)), stores buffersize / sizeof(uint16t) in resultslength, and corrects the loop bound to a post-decrement so exactly the available number of slots may be written.
The SMBus driver API exposed smbussmbalertremovecb() and smbushostnotifyremovecb() as Zephyr syscalls. Their verifiers in drivers/smbus/smbushandlers.c validated only the dev argument with KSYSCALLOBJ(dev, KOBJDRIVERSMBUS) and forwarded the caller-supplied struct smbuscallback cb pointer into kernel-mode driver code without any KSYSCALLMEMORYREAD/KSYSCALLMEMORYWRITE validation. A companion change in 2023 had already removed the matching smbussmbalertsetcb() / smbushostnotifysetcb() syscalls for this reason, but the two removal syscalls were left exposed.
On a build with CONFIGUSERSPACE=y, CONFIGSMBUS=y and a driver implementing the callback operations (drivers/smbus/intelpchsmbus.c with CONFIGSMBUSINTELPCHSMBALERT/CONFIGSMBUSINTELPCHHOSTNOTIFY, or drivers/smbus/smbusstm32.c with CONFIGSMBUSSTM32SMBALERT), any user-mode thread that has been granted the SMBus device object can invoke these syscalls with an arbitrary pointer. The value reaches smbuscallbackremove() in drivers/smbus/smbusutils.h, which uses it as a node identity against the kernel's sysslistt of registered callbacks.
The consequence is that an unprivileged thread can unregister an SMBALERT or Host Notify callback that a supervisor-mode component registered, silently disabling alert handling for the rest of the system; because Zephyr images have fixed symbol addresses and the syscall returns 0 on a hit versus -ENOENT on a miss, the target address is both derivable and searchable. In builds with CONFIGASSERT=y the ASSERT(callback->handler, ...) check additionally dereferences the caller-supplied address in supervisor mode, so a bogus pointer raises a kernel-mode fault and a fatal system error, and the fault/no-fault outcome discloses which addresses are mapped.
The fix removes both syscall entry points, demoting the two functions to ordinary static inline calls so that callback list manipulation is available only to supervisor-mode code. There is no impact on builds without CONFIGUSERSPACE, and no impact on configurations that do not enable an SMBus driver with SMBALERT or Host Notify support.
The MCUmgr SMP-over-console transport decodes a base64 frame, reads a 16-bit packet length from it, verifies a CRC and then unconditionally strips the trailing CRC with rxctxt->nb->len -= 2U; in mcumgrserialprocessfrag() (subsys/mgmt/mcumgr/transport/src/serialutil.c). mcumgrserialextractlen() accepted any declared length, including 0 and 1, and a packet declaring length 0 passes the checksum test for free because crc16itut() over zero bytes returns the zero seed. Since netbuf::len is a uint16t, the subtraction underflows and the buffer is handed to SMP claiming roughly 65 KB of payload while its data area is only CONFIGMCUMGRTRANSPORTNETBUFSIZE bytes (default 384).
The trigger is a single unauthenticated 7-byte line on the management console — the 0x06 0x09 packet marker followed by the base64 group AAA= and a newline — delivered to any transport built on this helper: CONFIGMCUMGRTRANSPORTUART (smpuart.c) or CONFIGMCUMGRTRANSPORTSHELL (smpshell.c), both of which select MCUMGRTRANSPORTSERIALHASSMPOVERCONSOLE. No prior session state, fragmentation or credentials are required to trigger the underflow, and the malformed frame is mishandled before any command handler or command-level access control runs. The attacker only needs write access to that console, which on many boards is a USB CDC-ACM port rather than a bare UART header.
With the inflated length, smpprocessrequestpacket() in subsys/mgmt/mcumgr/smp/src/smp.c loses its bound: cbornbreaderinit() gives the CBOR decoder a ~65 KB window into a 384-byte buffer, and each request header's nhlen is checked only against the inflated length. On its own the 7-byte frame re-parses whatever stale bytes the reused pool buffer still holds, typically a replay of the previously received request followed by a parse error, without leaving the buffer. Because the transport is unauthenticated, though, the attacker also controls the frames sent before the trigger, and can stage buffer contents so that a request succeeds with an nhlen larger than the buffer; netbufpull(), guarded only by ASSERTNOMSG, then moves the parse cursor out of bounds and the loop reads further headers and CBOR from adjacent memory. The consequence is an out-of-bounds read that can fault the MCUmgr thread (denial of service); memory disclosure is also possible, since the default-enabled os echo handler (CONFIGMCUMGRGRPOSECHO) decodes its string inside that window and copies it into its response. There is no integrity gain beyond what the unauthenticated transport already permits.
The fix rejects any declared packet length of two bytes or fewer in mcumgrserialextractlen(), so the CRC-strip subtraction can no longer underflow. The identical pattern remains in the test-only loopback transport subsys/mgmt/mcumgr/transport/src/smpdummy.c (CONFIGMCUMGRTRANSPORTDUMMY), which has no external input path and therefore carries no practical exposure.
The native BSD-socket layer recorded a pending asynchronous socket error by type-punning it into struct netcontext's void userdata field (ctx->userdata = INTTOPOINTER(-status) in zsockacceptedcb(), zsockreceivedcb(), zsockconnectedcb() and zsockclosectx() in subsys/net/lib/sockets/socketsinet.c), reading it back with POINTERTOINT(). That same field is owned by the network stack for listening TCP contexts: nettcpaccept() stores the parent context pointer there and the TCP core passes it back to the registered accept callback. A failed accept therefore left a small integer (an errno value) where the stack expected a struct netcontext .
When the network interface carrying a listening TCP socket goes down, closetcpconn() in subsys/net/ip/tcp.c invokes the accept callback with -ENETDOWN and the context's userdata. In v4.3.0 the callback was not disarmed afterwards, so a second interface-down event forwarded the previously stored errno to zsockacceptedcb(), which dereferenced it as the parent context and performed several stores through it (sockseterror()'s read-modify-write of socketdata, kfifocancelwait(&parent->recvq)) — the crash described in the fix's commit message. v4.3.1 and v4.4.x carry a later change clearing conn->acceptcb after the error callback (269cb8823d3 on the v4.3 branch, 913fae5169425550f2364655298fceb79b320066 on main), which closes that repeat path; on those releases the poisoned cookie remains reachable only by a narrower race, a handshake completing alongside the interface-down still passing the stale cookie to kfifoput(&parent->acceptq, ...), and by getsockopt(SOERROR), which reads the field back unconditionally.
On v4.3.0 an application that keeps a listening TCP socket open across repeated link-down events is sufficient to reach the defect; the triggering condition is a network-interface state change, not attacker-supplied packet data, so the practical attacker is one able to force the link down repeatedly (for example an adjacent attacker disrupting a wireless link) or one with local/physical access. Because both the faulting address and the stored data are fixed small constants derived from the errno value, the outcome is a wild-pointer access leading to a kernel fatal error — a denial of service (device crash or reset) rather than an attacker-directed memory corruption.
The fix stores the pending error in a dedicated netcontext.sockerror field and converts every producer and consumer to sockseterror()/sockgeterror(), leaving userdata untouched. As a side effect it also stops getsockopt(SOERROR) — which is evaluated unconditionally — from returning the kernel address held in userdata to a userspace application.
The ADC API requires each driver to reject a sampling sequence whose destination buffer is too small: the buffersize field of struct adcsequence in include/zephyr/drivers/adc.h documents that "the driver must ensure that samples are not written beyond the limit and it must return an error if the buffer turns out to be not large enough". The ADI MAX32 driver did not honour that contract. startread() in drivers/adc/adcmax32.c compared buffersize, a byte count, against a sample count ((1 + extrasamplings) channels), ignoring sizeof(uint16t), so it accepted a buffer half the required size. The samples are then stored through the uint16t data->buffer by WrapMXCADCGetData(), which writes two bytes per sample and advances the pointer by one uint16t: in adcmax32startchannel() for synchronous reads, and in adcmax32isr() for asynchronous ones. A sequence selecting two channels with a two-byte buffer, for example, passes the check and has its second sample written past the end of the buffer.
On a build with CONFIGUSERSPACE, adcread() and adcreadasync() are system calls. The handler in drivers/adc/adchandlers.c copies the sequence in from user memory, verifies only that [buffer, buffer + buffersize) is writable by the calling thread, and rejects a user-supplied options->callback; it deliberately leaves the size arithmetic to the driver. A user-mode thread that has been granted access to a MAX32 ADC device object therefore fully controls channels, buffer, buffersize and options->extrasamplings, and can make the driver write twice as many bytes as its buffer holds. Because the check scales with extrasamplings, the overrun equals the length of the buffer itself, up to channels 65536 bytes past its end, since the sample pointer is only rewound on a repeat sampling, never on the extra samplings of a sequence.
The resulting stores are performed by the driver in kernel mode (in the system call itself, the ADC context timer, or the ADC interrupt handler for asynchronous reads), where the MPU does not restrict the thread's memory domain, so the write walks linearly out of the user partition and into adjacent memory such as other partitions, kernel data or thread stacks. The impact is kernel-memory corruption of attacker-chosen length at an attacker-chosen offset, a plausible privilege-escalation and denial-of-service primitive from an unprivileged user-mode thread. Builds without CONFIGUSERSPACE are affected only as a caller-side robustness defect, since the application itself supplies the buffer.
The fix replaces that check in startread() with a call to the new shared helper adcsequencevalidatebuffer() in drivers/adc/adccommon.c, passing sizeof(uint16t) as the sample size. The helper computes activechannels sizeof(uint16t) (1 + extrasamplings) and returns -ENOMEM before any sampling is started.
netifipv6calcreachabletime() in subsys/net/ip/netif.c derives a randomized ND reachable time from ipv6->basereachabletime as minreachable + sysrand32get() % (maxreachable - minreachable), where minreachable = base/2 and maxreachable = 3base/2 using integer division. When basereachabletime is 1, both minreachable and the modulus collapse so the function returns 0, and netifipv6setreachabletime() stores that 0 into ipv6->reachabletime.
The basereachabletime is attacker-controlled: handlerainput() in subsys/net/ip/ipv6nbr.c accepts the Reachable Time field of an incoming Router Advertisement whenever it is nonzero and <= MAXREACHABLETIME, so a single unauthenticated, link-local RA carrying a Reachable Time of 1 drives the computed reachable time to 0. Router Advertisements are unauthenticated by default and require only adjacency to the target link.
When a neighbor is subsequently confirmed reachable, netipv6nbrsetreachabletimer() reads the value and executes NETASSERT(time, "Zero reachable timeout!"). On builds with CONFIGASSERT enabled this triggers a fatal kernel assertion — a remote denial of service; on builds without assertions the reachable timer is armed with KMSEC(0) and fires immediately, forcing reachable neighbors into perpetual re-solicitation (STALE), degrading Neighbor Discovery. The impact is limited to availability; there is no memory-safety, confidentiality, or integrity consequence.
The virtio PCI driver (drivers/virtio/virtiopci.c) parses a device's PCI capability list during driver initialization. In virtiopcireadcap() the device-supplied capability length byte caplen (read from PCI config space via pcieconfread()) was only checked with assert(tmp.caplen == capstructsize). That assert resolves to ASSERTNOMSG(), gated by CONFIGASSERT, which defaults off in production builds, so the value reached the copy logic completely unvalidated.
The length then drives a loop that copies extra capability dwords into a fixed-size stack buffer supplied by the caller. A caplen below the 24-byte base struct virtiopcicap underflows the unsigned extradatawords count to a near-SIZEMAX value, producing an effectively unbounded stack write; a caplen above the caller's buffer (up to 255) writes up to roughly 228 bytes of device-controlled data past the buffer. Both are out-of-bounds writes of attacker-controlled content executed in kernel mode during boot-time device probe.
The input originates from the virtio device. In the common deployment where Zephyr runs as a guest under a hypervisor, the device backend is the host, which already fully outranks the guest, so the bug yields no privilege escalation. The exploitable case is a virtio device that is untrusted relative to the Zephyr kernel — an untrusted or physical/passthrough virtio PCIe device on a bare-metal system, or a confidential-computing posture where the guest must defend against the host — where a malicious device can corrupt the kernel stack and potentially achieve code execution or a crash.
The fix replaces the compiled-out assert with a runtime range check rejecting caplen outside [sizeof(struct virtiopcicap), capstructsize] before any arithmetic or copy.
Zephyr's dynamic kernel-object disposal path unrefcheck() in kernel/userspace/userspace.c frees an object's storage (kfree(dyn->data)) once its reference count reaches zero, after running a per-object-type cleanup. The cleanup switch handled only KOBJMSGQ and KOBJSTACK; there was no KOBJTIMER case. A dynamically-allocated, initialized, and armed ktimer keeps its embedded struct timeout dnode linked in the global timeout queue (timeoutq), so freeing the timer storage without cancelling the timeout leaves a dangling node in that queue.
When the timer next expires, the timeout machinery walks timeoutq and invokes ztimerexpirationhandler() on the freed node, dereferencing and writing freed (and reusable) kernel heap in kernel/ISR context. This is a deterministic use-after-free that does not depend on SMP: the queued node is simply never unlinked at free time.
The disposal is reachable from an unprivileged user thread under CONFIGUSERSPACE + CONFIGDYNAMICOBJECTS: a thread that holds the last permission on such a timer drops it via the kobjectrelease() syscall (or by exiting, through kthreadpermsallclear()), and can arm the timer itself via the ktimerstart() syscall. The free and the expiration handler run at kernel privilege while the actor is a user thread, so the bug is a sandbox-escape memory-corruption primitive usable for privilege escalation. The fix adds ktimercleanup() (cancel the timeout and wait for any in-flight handler) and calls it for KOBJTIMER before freeing.
Zephyr's IPv6 forwarding path re-sent routed unicast packets without ever decrementing the IPv6 hop limit. Both routing branches of ipv6routepacket() (subsys/net/ip) were affected: the explicit-route path (netroutepacket()) and the on-link cross-interface path (netroutepacketif()). Each set the packet forwarding flag and called netsenddata() with the hop limit untouched and no expiry check.
Per RFC 8200 the hop-limit decrement is the mechanism that bounds packet lifetime and terminates routing loops; without it, a device acting as an IPv6 router relays looping packets indefinitely. An on-path attacker who can induce or exploit a transient L3 loop turns it into a permanent forwarding storm, causing CPU/bandwidth resource exhaustion (availability DoS) on the forwarder and adjacent links; path-discovery and loop diagnostics that rely on hop-limit expiry are also defeated.
Affected configurations. In every affected release the forwarding path is reached via CONFIGNETROUTE (enabled by default when CONFIGNETIPV6NBRCACHE is set), together with CONFIGNETROUTING for cross-interface routing. Note that CONFIGNETIPV6FORWARDING and CONFIGNETIPV4FORWARDING — which appear in the fix and in this advisory's evidence notes — were introduced after v4.4.0, when the routing options were split and renamed; they do not exist in any affected release. When auditing a v4.4.1-or-earlier configuration, look for CONFIGNETROUTE and CONFIGNETROUTING.
IPv4 is not affected in any release. The IPv4 forwarding path (netrouteipv4packet() in routeipv4.c) was added after v4.4.0 and has never shipped in a release. Its TTL decrement and IPv4 header-checksum recomputation landed on main as part of the same fix, so the evidence notes below discuss it, but no released version is reachable by way of IPv4.
Affected releases are v1.8.0 through v4.4.1: v1.8.0 introduced netroutepacket() and v2.2.0 added netroutepacketif(), and neither decremented the hop limit. v4.3.1 carries the explicit-route fix but not the on-link one, so it is affected as well. Fixed on main by 7d8f1afa7345 (explicit-route path) and 589eadc74efa (on-link path).
The MCTP-over-I2C+GPIO target binding in Zephyr (subsys/pmci/mctp/mctpi2cgpiotarget.c) processes pseudo-register writes from an I2C bus master byte-by-byte in mctpi2cgpiotargetwritereceived() without validating the order or the receive buffer. In the affected versions the MCTPI2CGPIORXMSGADDR (data) handler dereferences and writes through b->rxpkt without checking that the receive buffer was allocated: a controller that selects the data register and writes a byte without first sending the length register (which is what allocates the buffer) causes a write of an attacker-chosen byte through a NULL/unallocated mctppktbuf pointer (i.e. into a small attacker-advanceable offset above address 0), producing memory corruption or a hard fault.
The same handler also performs a write-then-check bounds test, allowing a one-byte heap overflow at data[255] when more than 255 data bytes are sent.
Because the I2C target callback is invoked with raw bytes supplied by whatever device is the bus master and the binding performs no authentication, a malicious or malfunctioning controller on the bus can trigger these without any prior protocol state, leading to memory corruption and/or denial of service on the target device.
The vulnerable code was introduced when the I2C+GPIO target binding was added and shipped in Zephyr v4.3.0 and v4.4.0. The fix defers allocation to the first data byte with a NULL check, treats a missing length as a zero-sized packet rejected by libmctp, and moves the bounds check before the store.
parseipv4() in subsys/net/ip/utils.c (reached via netipaddrparse() for strings of the form "a.b.c.d:port") copies the port substring into a fixed 17-byte stack buffer (char ipaddr[NETIPV4ADDRLEN + 1]) using a length of strlen - end - 1, where strlen is the full, unbounded input length and end is only the (<=15-byte) offset of the ':' delimiter. Because the destination size is never consulted, a crafted address string with a long suffix after the colon (e.g. "1.2.3.4:" followed by hundreds of bytes) causes an out-of-bounds stack write whose length and contents are fully attacker-controlled (memcpy of the suffix plus a trailing NUL), enabling memory corruption and at minimum a denial of service, and potentially control-flow hijack. The parser is reached from the standard socket API (zsockgetaddrinfo / literal-address resolution), DNS server-string configuration, and the eswifi Wi-Fi co-processor DNS-response path, so an application that resolves a network-influenced address string is exposed. The bug was introduced when the parser was added (Zephyr v1.9.0) and shipped in all releases through v4.4.0. The fix removes the unbounded copy and validates the port length before copying into a small dedicated buffer. Note: the equivalent IPv6 "[addr]:port" path in parseipv6() retains the same unbounded copy at this commit and remains a separate, still-reachable instance of the defect.
Zephyr's dynamic kernel-object tracking (kernel/userspace/userspace.c, formerly kernel/userspace.c) maintains a doubly-linked list (objlist) of dynamically allocated kernel objects. Iteration over this list in kobjectwordlistforeach() was performed under listslock using the SAFE iterator (which caches the next node), but list removal and freeing of nodes was performed under different, disjoint spinlocks: objfreelock in kobjectfree() and objlock in unrefcheck(). On an SMP system, while one CPU iterated objlist under listslock, another CPU could unlink and kfree() the dynobj node that the iterator had cached as its next pointer, causing the iterator to dereference freed kernel memory (use-after-free / dangling list traversal). All of the racing operations are reachable from unprivileged user-mode threads via system calls: kobjectalloc/kobjectallocsize and kobjectrelease drive removals through unrefcheck() (under objlock), while kthreadabort and thread creation drive the iteration through kthreadpermsallclear()/kthreadpermsinherit() (under listslock). A deprivileged user thread on a CONFIGSMP + CONFIGUSERSPACE build can therefore corrupt the kernel's object-tracking structures across the userspace security boundary, yielding kernel memory corruption (potential privilege escalation) or a kernel crash (denial of service). The fix removes objfreelock and serializes every objlist modification under listslock, including holding it across find+remove in kobjectfree() and around unrefcheck() in kthreadpermsclear(). Affects CONFIGSMP+CONFIGUSERSPACE+CONFIGDYNAMICOBJECTS configurations; the defect dates to the 2019 spinlockification (commit 8a3d57b6cc6, first released in v1.14.0) and shipped through v4.4.0.
In Zephyr's WireGuard subsystem (subsys/net/lib/wireguard), wgprocessdatamessage() in wgcrypto.c linearizes an inbound transport-data payload into a fixed pool buffer of CONFIGWIREGUARDBUFLEN bytes before decryption. The call netbuflinearize(buf->data, datalen, pkt->buffer, ..., datalen) passed the attacker-derived datalen as both the destination capacity and the copy length, defeating the function's internal len = min(len, dstlen) bound. datalen is derived from the received UDP datagram length and is only lower-bounded by wgctrlrecv() (no upper bound). When datalen exceeds CONFIGWIREGUARDBUFLEN — e.g. when the buffer length is lowered below the link MTU, on links with MTU above the buffer size, or via reassembled IPv4/IPv6 fragments that exceed it — the underlying memcpy writes past the end of the pool buffer, an out-of-bounds write (CWE-787). The overflow occurs before the Poly1305 authentication check, so it requires only a valid receiver session index rather than a valid authenticator, and is reachable by a malicious or compromised peer (or an on-path attacker driving an established session) over the network, yielding remote memory corruption and at minimum a reliable denial of service. The defect was present in the WireGuard implementation shipped in Zephyr 4.4.0. The fix adds an explicit datalen > CONFIGWIREGUARDBUFLEN rejection and corrects the linearize call to pass netbufmaxlen(buf) as the destination capacity.