See how zephyr compares to other vendors in security performance
The HTTP client's debug helper printheaderfield() in subsys/net/lib/http/httpclient.c formatted an httpparser data-callback slice with a %s conversion: snprintk(output, len + 1, "%s", str). The parser hands its callbacks a pointer and a length into the caller's receive buffer, and those bytes are not NUL terminated. The size argument of snprintk() bounds only the destination, while the %s conversion measures the source first — lib/os/cbprintfcomplete.c uses strlen() when no precision is given, and lib/os/cbprintfnano.c uses strlen() unconditionally — so the read walks past the end of the header field and past the end of the receive buffer until it finds a zero byte.
The slice reaches the helper from onurl(), onheaderfield() and onheadervalue(), which the parser invokes on bytes that httpwaitdata() read straight off the socket into the application-supplied recvbuf. A malicious or compromised HTTP server, or an attacker able to answer a plaintext request, therefore controls both the header field and the bytes that follow it in the buffer, and can guarantee that no zero byte appears before the buffer ends, forcing the over-read. The in-tree tests/net/lib/httpclient suite reproduces the over-read under CONFIGASAN=y on an ordinary GET.
The impact is bounded. The whole helper body is compiled only when the HTTP client log module is explicitly set to Debug (CONFIGNETHTTPLOGLEVELDBG=y, which the global CONFIGLOGDEFAULTLEVEL does not set), so default builds are unaffected. No out-of-bounds write occurs and none of the over-read bytes are emitted into the log, so there is no information disclosure; the consequence is an out-of-bounds read that can fault and panic the device where the buffer abuts an unmapped or MPU/MMU-protected region.
The fix copies exactly the len bytes the parser offered with memcpy() and terminates them, instead of relying on a %s conversion (a %.s precision would not have sufficed, since cbprintfnano.c measures the source in full before applying it).
The HTTP/2 server in subsys/net/lib/http/httpserverhttp2.c clears the connection's stream binding at the start of every frame. handlehttpframeheader() executed client->currentstream = NULL unconditionally, but a CONTINUATION frame does not open a new stream and enterhttpframecontinuationstate() never re-bound one, so the rest of the header block was processed with client->currentstream unset. Two paths then write through the NULL pointer. A request for an unregistered resource, or a method the matched resource does not support, is answered by sendhttp2404() or sendhttp2405(), and both call sendheadersframe(), which unconditionally sets client->currentstream->headerssent = true. A POST, PUT or PATCH to a registered dynamic resource reaches handlehttp2dynamicresource(), which unlike its siblings (handlehttp2staticresource(), dynamicgetdeloptsreqv2(), dynamicpostputreqv2()) had no client->currentstream == NULL guard, and stores through the pointer with client->currentstream->currentdetail = dynamicdetail.
Any peer that can reach the server can trigger this without authentication or prior state. A single stray CONTINUATION frame with ENDHEADERS set, sent right after the client preface and requesting any unregistered path, is enough, because nothing rejected a CONTINUATION that no HEADERS frame had opened. A legitimate split header block (a HEADERS frame with ENDHEADERS clear whose payload ends on a header-field boundary, then a CONTINUATION with ENDHEADERS set) reaches the same state. The in-tree test testhttp2postheaderswithcontinuation() did not catch this because it splits the header block mid field, which takes the handleincompletehttpheader() shortcut that keeps the stream bound.
The result is a write to a fixed low address (a small offset into struct http2streamctx), which faults on mainstream targets (flash/vector-table region on Cortex-M, or a trap under MPU/MMU null-pointer detection), and the default fatal-error handler halts the system. Impact is an unauthenticated, remotely triggerable denial of service against any device running the HTTP/2 server: the 404 path needs no particular resource to be registered, and HTTP/2 is built into CONFIGHTTPSERVER with cleartext h2c accepted, so TLS does not prevent it. Neither the target address nor the written value is attacker-controlled, so there is no confidentiality or integrity impact. Applications built without CONFIGHTTPSERVER are not affected.
The HTTP/2 server in subsys/net/lib/http/httpserverhttp2.c clears the connection's stream binding at the start of every frame. handlehttpframeheader() executed client->currentstream = NULL unconditionally, but a CONTINUATION frame does not open a new stream and enterhttpframecontinuationstate() never re-bound one, so the rest of the header block was processed with client->currentstream unset. Two paths then write through the NULL pointer. A request for an unregistered resource, or a method the matched resource does not support, is answered by sendhttp2404() or sendhttp2405(), and both call sendheadersframe(), which unconditionally sets client->currentstream->headerssent = true. A POST, PUT or PATCH to a registered dynamic resource reaches handlehttp2dynamicresource(), which unlike its siblings (handlehttp2staticresource(), dynamicgetdeloptsreqv2(), dynamicpostputreqv2()) had no client->currentstream == NULL guard, and stores through the pointer with client->currentstream->currentdetail = dynamicdetail.
Any peer that can reach the server can trigger this without authentication or prior state. A single stray CONTINUATION frame with ENDHEADERS set, sent right after the client preface and requesting any unregistered path, is enough, because nothing rejected a CONTINUATION that no HEADERS frame had opened. A legitimate split header block (a HEADERS frame with ENDHEADERS clear whose payload ends on a header-field boundary, then a CONTINUATION with ENDHEADERS set) reaches the same state. The in-tree test testhttp2postheaderswithcontinuation() did not catch this because it splits the header block mid field, which takes the handleincompletehttpheader() shortcut that keeps the stream bound.
The result is a write to a fixed low address (a small offset into struct http2streamctx), which faults on mainstream targets (flash/vector-table region on Cortex-M, or a trap under MPU/MMU null-pointer detection), and the default fatal-error handler halts the system. Impact is an unauthenticated, remotely triggerable denial of service against any device running the HTTP/2 server: the 404 path needs no particular resource to be registered, and HTTP/2 is built into CONFIGHTTPSERVER with cleartext h2c accepted, so TLS does not prevent it. Neither the target address nor the written value is attacker-controlled, so there is no confidentiality or integrity impact. Applications built without CONFIGHTTPSERVER are not affected.
The entropy-device random number generator in subsys/random/randomentropydevice.c returned early with -ENODEV from randget() as soon as deviceisready() failed, before reaching the non-cryptographic system-timer fallback further down the same function. Because zimplsysrandget() returns void and discards that error — and the public sysrandget() syscall is itself declared void, so it cannot report failure — the caller's output buffer was left untouched and its previous contents were handed back as random data. The thin wrappers make this a plain uninitialized read: sysrand32get() declares uint32t ret;, calls sysrandget(&ret, sizeof(ret)) and returns ret.
The "not ready" state is a normal runtime condition rather than a fault. Entropy drivers initialise at PREKERNEL1 with CONFIGENTROPYINITPRIORITY (default 50), so any earlier initialiser — kernel objects run at priority 30 — observes an unready device; the sibling generator subsys/random/randomxoshiro128.c and zearlyrandget() in kernel/init.c both already carried an explicit timer fallback for exactly this case. The state is also permanent for the boot when an entropy driver's init hook fails, which several in-tree drivers can do on real hardware (entropystm32 on an RNG clock configuration error, entropypsacrypto when psacryptoinit() fails, entropynrfcracen, entropyesp32, entropyvirtio).
When this occurs, every non-cryptographic random value in the system is either a constant or stale memory. coapnexttoken() in subsys/net/lib/coap/coap.c fills a static buffer, so every CoAP token becomes the same .bss constant; tcpinitisn() in subsys/net/ip/tcp.c returns an uninitialized stack word as the TCP initial sequence number; DHCPv4/DHCPv6 transaction IDs, WebSocket masking keys and RTP SSRCs are similarly degraded, and arch/arm64/core/pac.c derives per-thread Pointer Authentication keys from the same source. Where the bytes are transmitted directly — SSH packet padding, the netmidi2 session nonce, a DTLS connection ID on builds without CONFIGCSPRNGENABLED — stale caller stack or heap contents are disclosed on the wire.
The fix initialises ret to -ENODEV and only calls entropygetentropy() when the device is ready, so the not-ready case now falls through to the timer fallback and the buffer is always written. The cryptographically secure path is unchanged: syscsrandget() still propagates the error to the caller and never silently degrades. An attacker cannot induce the unready state, but can observe and exploit the resulting non-random values remotely on an affected device.
The RTIO submission-queue verifier rtiovrfysqe() in subsys/rtio/rtiosyscalls.c (subsys/rtio/rtiohandlers.c before v4.3.0) accumulated the results of KSYSCALLMEMORY() into its validsqe flag with &=. That macro returns nonzero on failure (it is built on KSYSCALLVERIFYMSG(), which yields !(expr)), so every buffer permission check contributed the opposite of its result. A submission whose tx.buf, rx.buf or txrx.txbuf/txrx.rxbuf pointer was not accessible to the calling thread was accepted, while a legitimate, thread-owned buffer caused the calling thread to be killed with a kernel oops.
The verifier runs on behalf of the rtiosqecopyingethandles() system call (also reached through the inline rtiosqecopyin() wrapper), so it is driven directly by untrusted user-mode input on any image built with CONFIGUSERSPACE. A user thread that has been granted an RTIO context and an rtioiodev object — the intended usage of RTIO userspace support, established via rtioaccessgrant() — can submit an SQE whose buffer pointer and length name arbitrary kernel memory; the object check on sqe->iodev is unaffected and still passes, and the crafted SQE is forwarded to the bus driver unchanged.
Downstream consumers dereference the pointer with supervisor privilege. The generic I2C and I3C RTIO fallback handlers (drivers/i2c/i2crtiodefault.c, drivers/i3c/i3crtiodefault.c) copy sqe.rx.buf/sqe.tx.buf into struct i2cmsg and call i2ctransfer(), and the vendor SPI/I2C RTIO drivers behave equivalently. An RTIOOPRX submission therefore has peripheral data written into kernel memory of the attacker's choosing (arbitrary kernel write), an RTIOOPTX submission clocks kernel memory out over the bus (kernel memory disclosure), and a pointer to unmapped memory faults the kernel. This is a full escape from the user-mode isolation boundary that CONFIGUSERSPACE exists to enforce.
The defect was masked for years because the in-tree user-mode RTIO tests only exercise RTIOOPNOP and mempool-backed reads, neither of which reaches the affected checks. The fix negates each KSYSCALLMEMORY() result and additionally validates the TXRX transmit buffer read-only, matching the SPI, I2C and I3C handlers.
Two RTIO syscall verifiers, in subsys/rtio/rtiosyscalls.c (subsys/rtio/rtiohandlers.c before v4.3.0), did not validate user-supplied pointers before the kernel wrote through them.
zvrfyrtiosqecancel() performed no validation at all. It passed the caller's struct rtiosqe pointer straight to zimplrtiosqecancel(), which converts it with CONTAINEROF to a struct rtioiodevsqe, ORs RTIOSQECANCELED (BIT(3)) into its 16-bit flags field in supervisor mode, then loads its next pointer and repeats until that pointer is NULL. The syscall takes no RTIO context argument, so it needs no object grant: any user-mode thread on a CONFIGUSERSPACE build can set that bit in a 16-bit word at an address of its choosing in kernel memory, and the kernel continues the write along whatever pointer chain it finds at the corresponding offset. Setting chosen bits in kernel data structures, such as object permission or thread state fields, can be used to escalate privilege from user mode.
zvrfyrtiosqecopyingethandles() validated its RTIO context and the input SQE array, but only NULL-checked the handle out-parameter before writing a kernel SQE pointer through it, with no KSYSCALLMEMORYWRITE check. A user thread that holds a granted RTIO context can therefore make the kernel store a pointer value at an arbitrary address.
The fixes validate the handle out-parameter as writable, bound the cancel handle to real submission-pool storage, and require permission on the RTIO context that owns the matching pool. Without that last check a bounded handle still let any user thread cancel submissions belonging to other contexts, silently aborting their I/O. Systems built without CONFIGUSERSPACE or without CONFIGRTIO are unaffected.
Two RTIO syscall verifiers, in subsys/rtio/rtiosyscalls.c (subsys/rtio/rtiohandlers.c before v4.3.0), did not validate user-supplied pointers before the kernel wrote through them.
zvrfyrtiosqecancel() performed no validation at all. It passed the caller's struct rtiosqe pointer straight to zimplrtiosqecancel(), which converts it with CONTAINEROF to a struct rtioiodevsqe, ORs RTIOSQECANCELED (BIT(3)) into its 16-bit flags field in supervisor mode, then loads its next pointer and repeats until that pointer is NULL. The syscall takes no RTIO context argument, so it needs no object grant: any user-mode thread on a CONFIGUSERSPACE build can set that bit in a 16-bit word at an address of its choosing in kernel memory, and the kernel continues the write along whatever pointer chain it finds at the corresponding offset. Setting chosen bits in kernel data structures, such as object permission or thread state fields, can be used to escalate privilege from user mode.
zvrfyrtiosqecopyingethandles() validated its RTIO context and the input SQE array, but only NULL-checked the handle out-parameter before writing a kernel SQE pointer through it, with no KSYSCALLMEMORYWRITE check. A user thread that holds a granted RTIO context can therefore make the kernel store a pointer value at an arbitrary address.
The fixes validate the handle out-parameter as writable, bound the cancel handle to real submission-pool storage, and require permission on the RTIO context that owns the matching pool. Without that last check a bounded handle still let any user thread cancel submissions belonging to other contexts, silently aborting their I/O. Systems built without CONFIGUSERSPACE or without CONFIGRTIO are unaffected.
The canaddrxfiltermsgq() system call installs a CAN RX filter whose callback (canmsgqput() in drivers/can/cancommon.c) is handed the caller's struct kmsgq as userdata. The controller driver stores that raw pointer in its filter table and dereferences it from the RX path every time a matching frame arrives. The system call verifier zvrfycanaddrxfiltermsgq() in drivers/can/canhandlers.c validated the queue only with KSYSCALLOBJ(), which is a point-in-time check of type, initialization and caller permission and says nothing about the object's lifetime.
On a build with CONFIGUSERSPACE=y and CONFIGDYNAMICOBJECTS=y, an unprivileged user-mode thread that has been granted access to a CAN device can obtain a message queue whose lifetime it fully controls: kobjectalloc(KOBJMSGQ) followed by kmsgqallocinit(). The lifetime of such an object is driven solely by its permission bitmask, and a pointer retained by the kernel is not represented there. After registering the filter, the thread calls kobjectrelease() — or simply terminates — and unrefcheck() frees the struct kmsgq storage while the CAN driver still holds the pointer.
The next matching CAN frame causes kmsgqput() to run in supervisor context against freed memory that the attacker can have groomed with another kernel allocation: the frame payload is memcpy()d through the reused block's writeptr, and the queue's waitq (and pollevents under CONFIGPOLL) list nodes are manipulated. This yields kernel-heap corruption reachable from a user-mode thread, defeating the isolation CONFIGUSERSPACE is intended to provide; the same dangling pointer also arises benignly when an application thread exits without calling canremoverxfilter().
The fix validates the queue with KSYSCALLOBJSTATIC(), which additionally rejects objects carrying KOBJFLAGALLOC, so only statically defined queues (CANMSGQDEFINE()) may be registered from user mode. Supervisor-mode callers bypass the verifier and must themselves keep the queue valid until the filter is removed.
The Bluetooth host's GATT client passed the value of a received Handle Value Notification, Handle Value Indication or Multiple Handle Value Notification to btgattsubscribeparams::notify callbacks using only the length derived from the received ATT PDU. btattrecv() in subsys/bluetooth/host/att.c validates only a minimum PDU length and does not enforce the negotiated ATT MTU, and the fixed ATT L2CAP channel in subsys/bluetooth/host/l2cap.c applies no rx.mtu check either, so the only ceiling was the ACL reassembly buffer CONFIGBTBUFACLRXSIZE. Values longer than the 512-octet maximum attribute length defined by the Bluetooth Core Specification (Vol 3, Part F, Section 3.2.9) could therefore be handed to subscribers that size their buffers to that documented limit.
A remote peer that a device has connected and subscribed to — a malicious, spoofed or compromised GATT server within radio range — triggers the flaw by sending a notification or indication whose value exceeds 512 octets on a subscribed handle. In the LE Audio unicast client this reaches a concrete out-of-bounds write: unicastclientepnotify() in subsys/bluetooth/audio/bapunicastclient.c copies the notification into the 512-byte attbuf of struct unicastclient with netbufsimpleaddmem() and no tailroom check whenever length equals btaudiogetmaxntfsize(), i.e. the negotiated ATT MTU minus three. notifyhandler() in subsys/bluetooth/audio/bapbroadcastassistant.c has the same shape. Reaching the overflow requires a build whose ATT MTU exceeds 515 octets (CONFIGBTL2CAPTXMTU and CONFIGBTBUFACLRXSIZE set for full-length attribute support); the shipped defaults are far smaller.
The overflow writes peer-controlled bytes past attbuf directly over the adjacent struct netbufsimple netbuf — its data, buf, len and size fields — which the subsequent long-read accumulation then uses as its destination, turning a small linear overflow into a controlled write. netbufsimpleadd() only asserts on insufficient tailroom unless CONFIGNETBUFHARDENING is enabled, and that option is disabled by default, so ordinary production builds perform the copy. Beyond the in-tree LE Audio clients, any application subscriber that sized its receive buffer to the specification's 512-octet limit was exposed to the same overflow.
The fix rejects notification, indication and multiple-notification values longer than BTATTMAXATTRIBUTELEN in callnotifycbandmaybeunsubscribe() in subsys/bluetooth/host/gatt.c before any subscriber callback is invoked, so no subscriber can be handed more than the specified maximum. Remaining tuples of a multiple handle value notification are still delivered and oversized indications are still confirmed.
In the Bluetooth Classic Hands-Free Profile Audio Gateway role, hfpagcreate() in subsys/bluetooth/host/classic/hfpag.c starts an SDP discovery toward the peer, which links the embedded ag->sdpparam.node into the SDP client's pending request list in subsys/bluetooth/host/classic/sdp.c. If the subsequent btrfcommdlcconnect() call fails, bthfpagconnect() cleaned up by memset()-ing the entire AG object while that node was still linked, and while ag->slcwork could still be queued on the system work queue. An in-flight SDP discovery cannot be cancelled — btsdpdiscovercancel() is declared in the public header but has no implementation — so the request stays in the list with its func, pool and uuid pointers zeroed.
The RFCOMM channel passed to bthfpagconnect() is taken from the peer's own SDP Hands-Free service record in the normal AG flow (btsdpgetprotoparam() performs no range validation, and samples/bluetooth/classic/handsfreeag feeds the value straight through), so a remote BR/EDR device in radio range that advertises an RFCOMM channel outside the valid range 0x01..0x1e deterministically forces btrfcommdlcconnect() to fail and reach the wipe. Reading the peer's SDP record requires no pairing, and the peer can also trigger the failure by dropping the ACL or racing an RFCOMM DLC on the same DLCI at the right moment.
When the SDP client next advances its request list, sdpclientdiscover() dereferences the zeroed parameters — netbufalloc(param->pool, …) with a NULL pool and param->uuid->type — and the result-notification paths call through the NULL params->func. Zeroing a linked node also truncates the request list behind a stale tail pointer, and re-arming the freed AG slot re-appends the same node and can produce a self-looping list that hangs the Bluetooth RX thread. The impact is a crash or hang of the Bluetooth stack; the written value is a constant zero at a fixed static address, so no attacker-controlled write is available.
Only builds with CONFIGBTCLASSIC and CONFIGBTHFPAG that use the Audio Gateway role are affected.
bthfphfconnect() in subsys/bluetooth/host/classic/hfphf.c allocates the per-connection HFP Hands-Free object with hfphfcreate(), which starts an SDP discovery by passing the object's embedded hf->sdpparam to btsdpdiscover(). That call links the caller-owned parameter block into the SDP client's request list (session->reqs in subsys/bluetooth/host/classic/sdp.c). If the subsequent btrfcommdlcconnect() then failed, the affected code wiped the entire object with (void)memset(newhf, 0, sizeof(newhf)) while that discovery was still outstanding, zeroing the syssnodet node that is still linked in the SDP client's list along with the request's func, uuid and pool pointers, and zeroing the hf->slcwork work item that may already be queued on the system workqueue. The discovery could not be retracted instead: btsdpdiscovercancel() is declared in include/zephyr/bluetooth/classic/sdp.h but has no implementation.
The failing branch is reachable with peer-supplied input. The documented use of bthfphfconnect() is to pass the RFCOMM channel read out of the remote Audio Gateway's SDP record; neither btsdpgetprotoparam() nor bthfphfconnect() range-checks that value, so a remote device advertising an HFP AG record with an RFCOMM channel outside 1-30 makes btrfcommdlcconnect() return -EINVAL and drives the local device into the wipe. The same branch is reached without any application misuse when the remote peer controls connection timing, for example when the RFCOMM session is in BTRFCOMMSTATEDISCONNECTING or the ACL is dropped between object creation and the RFCOMM call.
Once the object has been wiped, the Bluetooth host continues to use it: sdpclientallocbuf() dereferences session->param->pool, which is now NULL, and the SDP receive paths call session->param->func() through a null function pointer, neither of which is NULL-checked. Because the wipe also clears hf->acl, the pool slot is considered free and a retry or an incoming HFP connection re-appends the same still-linked list node, turning session->reqs into a self-referential list that the host's list walkers never leave. The result is a fault or an unrecoverable hang in the Bluetooth host thread; the stray indirect call is through an all-zero pointer whose target the attacker does not control, so the practical impact is denial of service.
The fix keeps the object allocated and marks it with a new BTHFPHFFLAGRELEASING flag, letting bthfphfdiscovercb() release it once the discovery completes instead of destroying it under the SDP client. Only builds with CONFIGBTCLASSIC and CONFIGBTHFPHF enabled are affected.
The Bluetooth host pre-allocates a btconn object (a "reservation") from the fixed aclconns[] pool for every connectable advertising set it starts, in leadvstartaddconn() in subsys/bluetooth/host/adv.c. When an incoming peripheral connection completes, findpendingconnect() in subsys/bluetooth/host/hcicore.c looked that reservation up using only the identity and the peer address, with a directed-then-undirected fallback, and had no knowledge of which advertising set actually terminated. With CONFIGBTEXTADVMAXADVSET > 1, where the LE Enhanced Connection Complete event is cached until the LE Advertising Set Terminated event identifies the set, this lookup can return the reservation belonging to a different, still-running advertising set.
The defect is reachable over the air by any adjacent device. If an application runs two connectable extended advertising sets on the same identity — one undirected and one directed at a peer P (the common "directed reconnect to a bonded central while still accepting new centrals" pattern) — a remote initiator whose address maps to P connecting on the undirected set causes the directed set's reservation to be consumed instead. The initiator address is chosen freely by the remote device and is not authenticated at link establishment, so no bonding, pairing or prior relationship is required to trigger the mis-attribution.
The undirected set's own reservation is then stranded: its BTADVENABLED flag has already been cleared, and leadvstopfreeconn() — the only path that returns a reservation to the pool — is reached only for sets that are still enabled, so btleextadvstop() returns early and the object is never released. Each occurrence permanently consumes one CONFIGBTMAXCONN slot until btdisable() or reboot. In application patterns that restart the directed advertising set after each disconnect, an attacker can repeat the sequence until the pool is exhausted, after which connectable advertising fails with -ENOMEM and the device can no longer accept connections. As a side effect the directed set keeps advertising without a reservation, so a genuine connection on it is dropped by bthcidisconnect(). The impact is availability only; no memory corruption or information disclosure results, and configurations with a single advertising set are unaffected.
The DNS stub resolver in subsys/net/lib/dns/resolve.c binds one UDP socket per configured server when the resolver context is initialised (dnsresolveinitlocked() -> registerdispatcher() -> dnsdispatcherregister()), with the local port left at 0 so the stack picks an ephemeral port exactly once. dnswrite() then sends every subsequent query from that same socket without re-binding, so the source port is constant for the lifetime of the resolver context while only the 16-bit transaction identifier (ctx->queries[i].id, drawn with sysrand16get()) varies per query. RFC 5452 section 9.2 requires both to vary; with a fixed port, an off-path attacker has to guess only the identifier.
The stable port is cheap to learn. Any single observed query reveals it permanently, and it can also be found by probing the ephemeral UDP range: subsys/net/ip/connection.c answers unmatched unicast UDP with an ICMP port-unreachable, so the resolver's socket is the one port in that range that stays silent. Acceptance of a reply is decided solely by getslotbyid() matching the transaction identifier and a CRC-16 of the queried name — the reply's source address is never checked (dispatchercb() does ARGUNUSED(addr)), so forged answers need not even be spoofed to appear to come from the configured server; they only need to reach the resolver's port.
An attacker with UDP reachability to the device (same LAN, Wi-Fi or mesh segment, or a globally routable device address) can therefore flood answers for a predictable query name across the 16-bit identifier space during a query window and race the legitimate server. A win makes the application resolve an attacker-chosen address, and with CONFIGDNSRESOLVERCACHE enabled the forged record is retained for its attacker-chosen TTL. The attack still requires winning that race, and applications that validate TLS certificates for the resolved endpoint are largely protected.
The fix draws a fresh local port before each query to a server that has nothing outstanding: dnsrandomizesourceport() unregisters the dispatcher, closes and reopens the socket, and re-registers it bound to port 0 so the system selects a new port, raising the per-query guessing space from roughly 16 to roughly 31 bits. Multicast DNS and LLMNR are excluded because their ports are protocol-fixed. The behaviour is controlled by CONFIGDNSRESOLVERRANDOMIZESOURCEPORT (default y).
The DHCPv4 client in subsys/net/lib/dhcpv4/dhcpv4.c maintained its transaction identifier (iface->config.dhcpv4.xid) as a counter rather than as a per-transaction random value. It was seeded once with sysrand32get() in dhcpv4startinternal(), incremented by one in dhcpv4senddiscover() and dhcpv4senddecline(), and never changed by dhcpv4sendrequest() — so every RENEWING and REBINDING exchange reused, unchanged, the identifier that had already appeared on the wire in the broadcast DHCPDISCOVER at boot, and where the identifier did move it moved predictably by one. RFC 2131 4.1 requires a single identifier per transaction, chosen so that clients are unlikely to collide.
This identifier is the client's only unpredictable reply-validation token. netdhcpv4input() accepts a BOOTP reply on op, a matching xid, and a matching chaddr (the interface MAC, which is public); the UDP callback is registered with a wildcard remote address and port, and no server-identifier or source-address check is applied. An attacker who learns one identifier — for example any host on the same segment that sees the broadcast DHCPDISCOVER, or a DHCP server or relay on a network the device previously joined — therefore retains a valid token for the device's whole uptime, and can predict the identifiers of later exchanges, without having to observe the unicast renew traffic or race the legitimate server.
A forged reply that passes those checks has effect regardless of client state, because dhcpv4parseoptions() applies its side effects before the state machine gate in dhcpv4handlereply(): the router option calls netifipv4setgw() and the DNS option calls dnsresolvereconfigure(). A single accepted datagram can thus repoint the device's default gateway and resolvers at attacker-controlled addresses, redirecting its traffic; during RENEWING or REBINDING a forged DHCPACK re-binds the lease with attacker-chosen parameters and a forged DHCPNAK removes the address and restarts configuration.
The fix draws a fresh sysrand32get() value at each point where a transaction begins — entering SELECTING, RENEWING and REBINDING, and at the INIT/INIT-REBOOT entry taken when an interface with a lease goes down — while REQUESTING and DECLINE keep the identifier of the exchange they belong to, as RFC 2131 requires. As a side effect the client also no longer discards a slow server's offer after retransmitting a discover, and the initial-discover backoff now draws its own randomness instead of deriving it from the identifier.
The DHCPv4 client in subsys/net/lib/dhcpv4/dhcpv4.c maintained its transaction identifier (iface->config.dhcpv4.xid) as a counter rather than as a per-transaction random value. It was seeded once with sysrand32get() in dhcpv4startinternal(), incremented by one in dhcpv4senddiscover() and dhcpv4senddecline(), and never changed by dhcpv4sendrequest() — so every RENEWING and REBINDING exchange reused, unchanged, the identifier that had already appeared on the wire in the broadcast DHCPDISCOVER at boot, and where the identifier did move it moved predictably by one. RFC 2131 4.1 requires a single identifier per transaction, chosen so that clients are unlikely to collide.
This identifier is the client's only unpredictable reply-validation token. netdhcpv4input() accepts a BOOTP reply on op, a matching xid, and a matching chaddr (the interface MAC, which is public); the UDP callback is registered with a wildcard remote address and port, and no server-identifier or source-address check is applied. An attacker who learns one identifier — for example any host on the same segment that sees the broadcast DHCPDISCOVER, or a DHCP server or relay on a network the device previously joined — therefore retains a valid token for the device's whole uptime, and can predict the identifiers of later exchanges, without having to observe the unicast renew traffic or race the legitimate server.
A forged reply that passes those checks has effect regardless of client state, because dhcpv4parseoptions() applies its side effects before the state machine gate in dhcpv4handlereply(): the router option calls netifipv4setgw() and the DNS option calls dnsresolvereconfigure(). A single accepted datagram can thus repoint the device's default gateway and resolvers at attacker-controlled addresses, redirecting its traffic; during RENEWING or REBINDING a forged DHCPACK re-binds the lease with attacker-chosen parameters and a forged DHCPNAK removes the address and restarts configuration.
The fix draws a fresh sysrand32get() value at each point where a transaction begins — entering SELECTING, RENEWING and REBINDING, and at the INIT/INIT-REBOOT entry taken when an interface with a lease goes down — while REQUESTING and DECLINE keep the identifier of the exchange they belong to, as RFC 2131 requires. As a side effect the client also no longer discards a slow server's offer after retransmitting a discover, and the initial-discover backoff now draws its own randomness instead of deriving it from the identifier.
Zephyr's OpenThread integration defined CONFIGOPENTHREADJOINERPSKD with default "J01NME" in modules/openthread/Kconfig.thread (previously subsys/net/l2/openthread/Kconfig, then subsys/net/l2/openthread/Kconfig.thread). J01NME is the example Joining Device Credential used throughout the public OpenThread and Thread commissioning documentation, including Zephyr's own samples/net/sockets/echoclient/README.rst, so it is not a secret. A firmware image built with CONFIGOPENTHREADJOINERAUTOSTART=y that never overrode the PSKd therefore shipped with a publicly known joining credential compiled in, with no build-time or run-time warning.
The PSKd is the only value that authenticates Thread commissioning: joiner and commissioner run a DTLS-PSK handshake keyed on it, after which the commissioner supplies the full operational dataset. In otconfigureinstance() (modules/openthread/openthread.c), a device with no stored dataset calls otJoinerStart() with the configured PSKd, and otjoinerstarthandler() calls otThreadSetEnabled() on success. An unauthenticated attacker within 802.15.4 radio range of an uncommissioned device (factory-fresh, after a factory reset, or after a settings erase) can run any stock Thread commissioner registered with J01NME, complete the handshake, and hand the device an attacker-controlled operational dataset.
The device then joins and enables Thread on the attacker's mesh. The attacker becomes its router, seeing, dropping, injecting and spoofing all of the device's IPv6 traffic and impersonating the peers the application expects, while the device is cut off from the network it was meant to join and stays mis-provisioned until it is reset. Only application-layer security with independently provisioned credentials survives. Exposure is limited to builds that enable joiner autostart and leave the PSKd unset; the option is not enabled by default and no in-tree sample uses the vulnerable combination.
The fix removes the default from CONFIGOPENTHREADJOINERPSKD and adds a BUILDASSERT() in modules/openthread/openthread.c, active only under CONFIGOPENTHREADJOINERAUTOSTART, that fails the build unless a 6-to-32-character PSKd is configured. Products should provision a per-device PSKd from factory data and call otJoinerStart() from the application rather than relying on a credential compiled identically into every unit.
The device multiplexer pseudo-driver in drivers/misc/devmux/devmux.c exposes two system calls, devmuxselectget() and devmuxselectset(). When CONFIGUSERSPACE is enabled, their user-mode verifiers zvrfydevmuxselectget() and zvrfydevmuxselectset() forwarded directly to the kernel implementations without performing any kernel-object validation — no kobjectaccesscheck() and no driver object check. Compounding this, DEVICEDTINSTDEFINE() registered devmux instances with a NULL driver API pointer, which causes scripts/build/genkobjectlist.py to skip them entirely, so a devmux device had no struct kobject and no permission on it could be granted or withheld in the first place.
Any thread running in user mode can reach the defect: device pointers such as DEVICEDTGET(DTNODELABEL(devmux0)) are compile-time constants available to application code, and the syscall accepted them from a thread that had never been granted access to the device. The kernel implementations do validate the pointer against the static devmux instance table and bounds-check the selection index, so this is not a memory-safety issue; the failure is purely one of authorization. Exposure requires CONFIGUSERSPACE=y together with the experimental CONFIGDEVMUX=y (which depends on CONFIGDEVICEMUTABLE) and a zephyr,devmux devicetree node.
An unprivileged user-mode thread can read and change which downstream device a multiplexer forwards to, bypassing the userspace object-permission model. Because a devmux forwards the data, configuration and API of the selected device — its documented use being zephyr,console and zephyr,shell-uart switching — an attacker can suppress console, log and shell output, redirect that output to a downstream device it is able to observe, or flip the selection underneath another thread that is legitimately using the multiplexer. The impact is bounded to the devicetree-fixed set of downstream devices; no out-of-bounds access or arbitrary pointer dereference is reachable.
The fix declares subsystem struct devmuxdriverapi and attaches a DEVICEAPI(devmux, devmuxapi) instance to each devmux device so that it becomes a tracked KOBJDRIVERDEVMUX kernel object, and adds KOOPS(devmuxinstget(dev) == SIZEMAX) plus KOOPS(kobjectaccesscheck(dev) != 0) to both verifiers.
The MCTP-over-I2C+GPIO target binding in subsys/pmci/mctp/mctpi2cgpiotarget.c emulates a small pseudo-register file, where the bus controller first writes a register number and then writes or reads payload bytes. The selected register (b->regaddr) and the data-phase flag (b->rxtx) were never cleared when the controller issued an I2C STOP, so after a completed transfer the target still had a data register selected while the packet buffer that register refers to had already been consumed and set to NULL by mctpi2cgpiotargetstop().
A controller on the bus can re-enter the same branch of mctpi2cgpiotargetstop() with a NULL buffer using two ordinary transactions and no authentication: first a normal packet write to MCTPI2CGPIORXMSGADDR followed by STOP (which delivers and frees b->rxpkt), then a bare read transaction followed by STOP. The failing readrequested() call leaves the stale state untouched and the I2C driver still delivers the STOP callback, which then calls mctpbusrx() with a NULL packet. The symmetric case exists on the transmit side, where the stale MCTPI2CGPIOTXMSGADDR selection dereferences b->txpkt->end after b->txpkt has been cleared and additionally signals b->txcomplete a second time, causing the next outbound packet to be dropped.
The dereference happens in the I2C target interrupt handler, so on any target that traps accesses to address 0 the result is a fatal exception and a device reset or halt — a denial of service inflicted by any device able to act as controller on the shared management bus (for example compromised firmware on a peer MCTP endpoint or on the BMC). On parts where address 0 is readable, the impact degrades to a malformed packet buffer being handed to the MCTP stack and lost transmit packets. No attacker-controlled value is written and no confidentiality or integrity boundary is crossed.
The fix resets b->regaddr to MCTPI2CGPIOINVALIDADDR and clears b->rxtx at every STOP, so a new register must be selected before any data phase can run again. Only builds that opt into the experimental CONFIGMCTP subsystem with CONFIGMCTPI2CGPIOTARGET=y are affected.
The OSDP Control Panel reply parser cpdecoderesponse() in subsys/mgmt/osdp/src/osdpcp.c decodes replies from a Peripheral Device into a stack-allocated struct osdpevent. For the REPLYKEYPPAD and REPLYRAW reply types, the payload length is taken from the received frame and the only validation performed is that the declared length is self-consistent with the number of payload bytes present in the frame. Because the attacker controls both the length field and the frame length, that check imposes no bound, and the subsequent memcpy() writes into the fixed 64-byte data arrays of struct osdpeventkeypress / struct osdpeventcardread (OSDPEVENTMAXDATALEN). The sibling REPLYFMT handler in the same switch already carried the missing > OSDPEVENTMAXDATALEN bound.
The defect is reached from cpprocessreply(), which reads bytes from the OSDP RS-485 channel and passes them through osdpphycheckpacket() and osdpphydecodepacket() before dispatching on the reply ID. Triggering it requires CONFIGOSDP with CONFIGOSDPMODECP and an application event callback registered (the normal Control Panel integration), plus a frame carrying a correct SOM, a valid CRC-16, and a matching 2-bit sequence number — all of which are computable or observable by anyone on the bus. No check ties the reply ID to the outstanding command, so an oversized REPLYKEYPPAD or REPLYRAW is accepted as the answer to a routine CMDPOLL. A malicious or compromised card reader, or anyone with access to the RS-485 wiring, can supply such a frame; CONFIGOSDPSCENABLED only helps while a secure channel is active, and a single unauthenticated frame carrying a bad MAC causes scdeactivate(), after which plaintext replies are again accepted.
With the default CONFIGOSDPUARTBUFFERLENGTH of 256, roughly 180 bytes of fully attacker-chosen data can be written past the end of the stack-allocated event object, over saved registers and the return address on typical Cortex-M and RISC-V frame layouts (Zephyr does not enable stack canaries by default). The impact ranges from a hard crash of the access-control panel to potential control-flow hijack in the context of the OSDP receive thread. The fix adds an OSDPEVENTMAXDATALEN bound to both handlers, capping each memcpy() at the size of its destination.
The UpdateHub firmware-over-the-air client parses the CoAP probe response with Zephyr's JSON library in zimplupdatehubprobe() (subsys/mgmt/updatehub/updatehub.c). The decoder treats every descriptor field as optional — jsonobjparse() returns a bitmask of the fields it actually decoded and never fails on a missing one — so a response that omits the "sha256sum" string leaves the corresponding const char in the zero-initialised struct respprobeanyboards / struct respprobesomeboards at NULL. Both metadata branches then computed sha256size = strlen(...objects.sha256sum) + 1 before any validation, dereferencing that NULL pointer; the SHA256HEXDIGESTSIZE test that follows operates on a value the bad read has already produced and cannot prevent it.
The metadata is supplied entirely by the network peer: probecb() accepts the datagram, runs coappacketparse() and copies the payload with no token matching and no cryptographic check. With CONFIGUPDATEHUBDTLS unset — the default — startcoapclient() uses plain CoAP over UDP on port 5683 with no peer authentication and resolves the server via unauthenticated DNS, so an on-path attacker, a DNS spoofer, or an off-path attacker who wins the race with a forged source address can deliver the malformed metadata. A probe response as simple as "objects":[[{}],[{}]] suffices, and no user interaction is required because updatehubautohandler() polls on CONFIGUPDATEHUBPOLLINTERVAL. When CONFIGUPDATEHUBDTLS=y the trigger narrows to a malicious or compromised update server.
The primitive is a read at address zero, not a write, so the impact is availability only. On targets where the low address range is unmapped or protected — MMU-based architectures, builds with CONFIGNULLPOINTEREXCEPTIONDETECTIONDWT/MPU/PMP, TrustZone non-secure images, and SoCs such as nRF54L whose 0x0–0x400 window is unmapped — the load faults and Zephyr's default fatal handler halts the system, giving a remotely re-triggerable denial of service on every poll. On classic Cortex-M targets where the vector table is readable at address zero the walk usually terminates immediately and the request is rejected by the length check, so the outcome is configuration- and SoC-dependent. There is no memory write, and the copied bytes are only compared against the locally computed image hash, so there is no information-disclosure channel.
The fix adds an explicit sha256sum == NULL test that returns UPDATEHUBMETADATAERROR ahead of the strlen() in both metadata branches. Debug builds (CONFIGUPDATEHUBLOGLEVEL=4) still format the same pointers with %s in a LOGDBG() loop that runs before the new checks, so that variant remains in debug configurations only.
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.
ethernetrecv() in subsys/net/l2/ethernet/ethernet.c validates only that the first netbuf of a received frame holds a plain 14-byte struct netethhdr. When the ethertype is 0x8100 and VLAN is enabled on the interface, the same buffer is reinterpreted as the 18-byte struct netethvlanhdr without re-checking the length: the VLAN TCI and the inner ethertype are read from offsets 14..17, past pkt->buffer->len, and hdrlen is raised to 18 so the subsequent netbufpull() removes more bytes than the buffer holds.
A single unauthenticated frame of 14 to 17 bytes carrying the VLAN ethertype triggers this. No knowledge of the node's VLAN configuration is needed: netethgetvlaniface() returns the base interface for the priority tag 0, and a broadcast destination address satisfies the "frame is for me" test, so a 16-byte broadcast frame with ethertype 0x8100 and TCI 0x0000 reaches the pull. Nothing in the receive path enforces the 60-byte Ethernet minimum — the USB CDC ECM and CDC NCM gadget classes in particular hand host-supplied frames of any non-zero length straight to netrecvdata().
With assertions enabled, netbufsimplepull() asserts and the node panics. In a production build (assertions and CONFIGNETBUFHARDENING off) the uint16t buffer length underflows to 65534 and buf->data is advanced past the end of the fragment's storage, so every later parser reads far outside the buffer and any netbufadd() on that fragment computes tailroom from the underflowed length. The result is a reliable remote denial of service and out-of-bounds access with information-disclosure and corruption potential.
Only configurations built with CONFIGNETVLAN=y that have a VLAN enabled on the receiving Ethernet interface, and whose MAC does not strip VLAN tags in hardware (ETHERNETHWVLANTAGSTRIP), are affected. The fix rejects the frame before the tag is read, so hdrlen is thereafter either 14 or 18 with both sizes validated against pkt->buffer->len.
ethernetrecv() in subsys/net/l2/ethernet/ethernet.c validates only that the first netbuf of a received frame holds a plain 14-byte struct netethhdr. When the ethertype is 0x8100 and VLAN is enabled on the interface, the same buffer is reinterpreted as the 18-byte struct netethvlanhdr without re-checking the length: the VLAN TCI and the inner ethertype are read from offsets 14..17, past pkt->buffer->len, and hdrlen is raised to 18 so the subsequent netbufpull() removes more bytes than the buffer holds.
A single unauthenticated frame of 14 to 17 bytes carrying the VLAN ethertype triggers this. No knowledge of the node's VLAN configuration is needed: netethgetvlaniface() returns the base interface for the priority tag 0, and a broadcast destination address satisfies the "frame is for me" test, so a 16-byte broadcast frame with ethertype 0x8100 and TCI 0x0000 reaches the pull. Nothing in the receive path enforces the 60-byte Ethernet minimum — the USB CDC ECM and CDC NCM gadget classes in particular hand host-supplied frames of any non-zero length straight to netrecvdata().
With assertions enabled, netbufsimplepull() asserts and the node panics. In a production build (assertions and CONFIGNETBUFHARDENING off) the uint16t buffer length underflows to 65534 and buf->data is advanced past the end of the fragment's storage, so every later parser reads far outside the buffer and any netbufadd() on that fragment computes tailroom from the underflowed length. The result is a reliable remote denial of service and out-of-bounds access with information-disclosure and corruption potential.
Only configurations built with CONFIGNETVLAN=y that have a VLAN enabled on the receiving Ethernet interface, and whose MAC does not strip VLAN tags in hardware (ETHERNETHWVLANTAGSTRIP), are affected. The fix rejects the frame before the tag is read, so hdrlen is thereafter either 14 or 18 with both sizes validated against pkt->buffer->len.
The Bluetooth host's ATT transmit path releases a buffer it does not own when signing fails. chansend() in subsys/bluetooth/host/att.c documents (and every other error exit implements) the contract that on failure the caller retains ownership of the netbuf; the BTATTOPSIGNEDWRITECMD branch instead called netbufunref(buf) when btsmpsign() returned an error and then returned that error to the caller. The callers act on the contract: processqueue() re-queues the buffer with kqueueprepend(), btattchansendrsp() puts it back on chan->txqueue, btattchanreqsend() restores it into the request, and attreqretry() unreferences it — so the already-freed buffer was re-queued, later dereferenced, and eventually freed again.
An ATT Signed Write Command is produced when an application calls btgattwritewithoutresponsecb() with sign set on an unencrypted connection. btsmpsign() fails whenever no local CSRK exists for the peer (-ENOENT), and whether one exists is negotiated with the peer: smpdistributekeys() stores a local CSRK only when the peer's pairing key-distribution field requests the signing key and bonding was agreed. A peer can therefore pair without accepting the CSRK, pair with No Bonding, remain unpaired, or keep the link unencrypted, and the victim's next signed write takes the failing path. Because struct netbuf's node field is simultaneously the linkage word for the ATT TX FIFO, for the deferred-destroy list used by the attpool destroy callback atttxdestroy(), and for the pool free list, the mishandled buffer corrupts these lists and can be re-issued by btattcreatepdu() to a new PDU while the ATT TX path still holds it.
The same branch also saved the netbufsimplestate only after signing, so a buffer restored after a btl2capsendpdu() failure kept its 12-octet signature; a subsequent retry re-signed it and appended a second signature with netbufadd(buf, 12) to a buffer that btattcreatepdu() sized for exactly one, writing past the end of the pooled buffer in builds where the netbuf tailroom assertion is compiled out.
Impact is host-side memory corruption in the Bluetooth stack: list corruption, a pooled ATT buffer with two owners (wrong PDU contents transmitted, or a double free when the queues are drained at disconnect), and a bounded out-of-bounds write on the retry path — typically observed as a crash or assertion failure. Exposure requires the deprecated CONFIGBTSIGNING to be enabled and an application that actually issues signed writes; in that configuration the triggering condition is under the remote peer's control.
The Link Layer Control Procedure (LLCP) implementation of the Zephyr software Bluetooth LE Controller retains the receive node that carried an accepted LLPHYUPDATEIND so that it can later be reused for the host notification when the update instant is reached (llcprxnoderetain() in subsys/bluetooth/controller/llsw/ullllcp.c, and the node is deliberately not recycled while marked NODERXTYPERETAIN). The invalid-PDU arms of llcplppurx() and llcprppurx() in subsys/bluetooth/controller/llsw/ullllcpphy.c completed the procedure via llcplrcomplete() / llcprrcomplete() without first releasing that retained node, so the procedure context — the only remaining reference to the node — was freed while the node was still held out of the receive pool.
A peer device on an established LE connection can drive this deterministically and without pairing or encryption. Against a peripheral it sends LLPHYREQ, receives LLPHYRSP, sends a valid LLPHYUPDATEIND with an instant a few connection events in the future (so the node becomes retained), and then, before the instant is reached, sends any other LL Control PDU such as LLLENGTHREQ; ullcprx() routes it to the active remote PHY Update procedure, which takes the invalid-PDU path. The mirror case applies to a locally initiated PHY Update followed by an LLREJECTIND.
In the default configuration (CONFIGBTASSERT and CONFIGBTCTLRASSERTDEBUG both default y) the violated invariant in llcplrcheckdone() / llcprrcheckdone() triggers a controller assertion, ending in koops() (or kpanic()) — a single crafted PDU sequence from radio range faults the device. With those assertions compiled out, each attempt silently leaks one receive PDU node and its memq link; because the controller receive pool is small (PDURXCNT, driven by CONFIGBTCTLRRXBUFFERS, which defaults to 1) and each attempt costs the attacker only a reconnect, a few repetitions exhaust the pool and leave Bluetooth inoperable until reboot. On releases v3.4.0 through v3.7.x the assertion is never reached, whatever the configuration, so every attempt leaks silently.
The impact is limited to availability: the orphaned node leaves no dangling pointer that is later dereferenced and is never delivered to the host, so there is no memory corruption or information disclosure. The same pull request applies the identical release to the Connection Update and CIS-create procedures, whose invalid-PDU arms had the same omission.
The Bluetooth LE host queues received L2CAP connection-oriented channel (CoC) data for deferred processing through a struct kwork embedded in the channel object (lechan->rxwork, handler l2caprxprocess()) whenever the channel uses a dynamic PSM (0x0080-0x00FF). Channel teardown in l2capchandestroy() in subsys/bluetooth/host/l2cap.c cancels the retransmission-timeout work and drains the RX FIFO, but never cancels rxwork. Because that work item was submitted to the system workqueue while HCI receive processing runs on the dedicated Bluetooth RX workqueue (CONFIGBTRECVWORKQBT, the default), a queued rxwork item can outlive the channel it points into.
A remote, unauthenticated peer with an established CoC channel triggers this by sending a data K-frame immediately followed by an L2CAP Disconnect Request. The K-frame submits rxwork to the system workqueue; because both workqueue threads are cooperative and the Bluetooth RX workqueue runs at the higher priority (KPRIOCOOP(CONFIGBTRXPRIO) versus CONFIGSYSTEMWORKQUEUEPRIORITY), the pending item cannot run before the following Disconnect Request is processed in ledisconnreq() -> l2capchandel() -> l2capchandestroy(). The stack then invokes the released() callback, which the API documents as meaning the stack has dropped all references and the application may free the channel memory.
The application therefore frees or re-accepts into an object that the system workqueue still holds in its pending list. If the memory is freed and reallocated, the workqueue later dereferences a list node and a handler function pointer read from reused memory; if the object is re-used for a later connection, l2capchanadd() calls kworkinit() on a still-enqueued work item, corrupting the workqueue's pending list so that unrelated work items are dropped or the queue spins on a looped list. A related variant lets l2caprxprocess() run concurrently with teardown, racing the netbuf unref of lechan->sdu and the clearing of chan->conn.
The fix routes the channel RX work to the Bluetooth workqueue - the same context in which every teardown path runs - and adds an explicit kworkcancel() of lechan->rxwork in l2capchandestroy(), so no reference to the channel survives the released() callback. Configurations without CONFIGBTL2CAPDYNAMICCHANNEL, or that only use SIG-assigned PSMs (EATT 0x0027, OTS 0x0025) which take the inline receive path, are not affected.
The Bluetooth LE host queues received L2CAP connection-oriented channel (CoC) data for deferred processing through a struct kwork embedded in the channel object (lechan->rxwork, handler l2caprxprocess()) whenever the channel uses a dynamic PSM (0x0080-0x00FF). Channel teardown in l2capchandestroy() in subsys/bluetooth/host/l2cap.c cancels the retransmission-timeout work and drains the RX FIFO, but never cancels rxwork. Because that work item was submitted to the system workqueue while HCI receive processing runs on the dedicated Bluetooth RX workqueue (CONFIGBTRECVWORKQBT, the default), a queued rxwork item can outlive the channel it points into.
A remote, unauthenticated peer with an established CoC channel triggers this by sending a data K-frame immediately followed by an L2CAP Disconnect Request. The K-frame submits rxwork to the system workqueue; because both workqueue threads are cooperative and the Bluetooth RX workqueue runs at the higher priority (KPRIOCOOP(CONFIGBTRXPRIO) versus CONFIGSYSTEMWORKQUEUEPRIORITY), the pending item cannot run before the following Disconnect Request is processed in ledisconnreq() -> l2capchandel() -> l2capchandestroy(). The stack then invokes the released() callback, which the API documents as meaning the stack has dropped all references and the application may free the channel memory.
The application therefore frees or re-accepts into an object that the system workqueue still holds in its pending list. If the memory is freed and reallocated, the workqueue later dereferences a list node and a handler function pointer read from reused memory; if the object is re-used for a later connection, l2capchanadd() calls kworkinit() on a still-enqueued work item, corrupting the workqueue's pending list so that unrelated work items are dropped or the queue spins on a looped list. A related variant lets l2caprxprocess() run concurrently with teardown, racing the netbuf unref of lechan->sdu and the clearing of chan->conn.
The fix routes the channel RX work to the Bluetooth workqueue - the same context in which every teardown path runs - and adds an explicit kworkcancel() of lechan->rxwork in l2capchandestroy(), so no reference to the channel survives the released() callback. Configurations without CONFIGBTL2CAPDYNAMICCHANNEL, or that only use SIG-assigned PSMs (EATT 0x0027, OTS 0x0025) which take the inline receive path, are not affected.
i2sesp32triggercheck() in drivers/i2s/i2sesp32.c validates the requested direction only for I2SDIRBOTH. The I2SDIRRX and I2SDIRTX branches read devcfg->rx.data->configured / devcfg->tx.data->configured without first checking the stream pointers. The device instantiation macro I2SESP32STREAMINIT() sets both .conf and .data to NULL for a direction the devicetree does not describe, so on an instance that wires only one direction — the normal shape for audio output or a worldsemi,ws2812-i2s LED strip — the other direction dereferences a NULL pointer instead of returning an error.
i2strigger() is a Zephyr syscall, and zvrfyi2strigger() in drivers/i2s/i2shandlers.c validates only the device object and the presence of the trigger API pointer; the dir argument is passed to the driver unvalidated. On a build with CONFIGUSERSPACE enabled, a user-mode thread that has been granted the I2S device can issue a single i2strigger() call naming the unwired direction and cause a load from address 0 in kernel mode. Among the Espressif parts that carry this driver, userspace is available in-tree only on RISC-V SoCs with CONFIGRISCVPMP, and v4.4.0 is the first release where that is buildable: the ESP32-C6 HPCORE selects RISCVPMP when it is not built for MCUboot. ESP32-C5 in v4.4.x carries the same PMP-region and userspace linker support but does not select RISCVPMP by default. Espressif Xtensa targets do not support Zephyr userspace, and in a non-userspace build the bad direction can only come from in-kernel application code.
The impact is limited to availability: the access is a read at offset 0 of the missing stream structure, so there is no attacker-controlled offset, no write primitive and no information disclosure. With the default fatal-error handler the resulting exception halts the system, giving an unprivileged user-mode thread a system-wide denial of service. The fix adds the same pointer check the I2SDIRBOTH branch already performed and returns -ENOSYS for a direction the instance does not implement; the driver's other entry points (i2sesp32configcheck(), i2sesp32configget(), i2sesp32read(), i2sesp32write()) already guarded the pointers, and a static audit found no equivalent unguarded path.
The driver defect is older than the affected range. The unguarded dereference is present from v4.2.0 (reached through i2sesp32triggerstream(), whose if (stream) guard tests the address of a struct member and is never false) and takes its present i2sesp32triggercheck() form in v4.3.0. No in-tree Espressif configuration before v4.4.0 can run a user-mode thread, so in v4.2.x and v4.3.x the direction argument can only come from trusted kernel code. Those releases carry the bug but are not listed as affected; the fix has also been merged to v4.3-branch as hardening.
i2sesp32triggercheck() in drivers/i2s/i2sesp32.c validates the requested direction only for I2SDIRBOTH. The I2SDIRRX and I2SDIRTX branches read devcfg->rx.data->configured / devcfg->tx.data->configured without first checking the stream pointers. The device instantiation macro I2SESP32STREAMINIT() sets both .conf and .data to NULL for a direction the devicetree does not describe, so on an instance that wires only one direction — the normal shape for audio output or a worldsemi,ws2812-i2s LED strip — the other direction dereferences a NULL pointer instead of returning an error.
i2strigger() is a Zephyr syscall, and zvrfyi2strigger() in drivers/i2s/i2shandlers.c validates only the device object and the presence of the trigger API pointer; the dir argument is passed to the driver unvalidated. On a build with CONFIGUSERSPACE enabled, a user-mode thread that has been granted the I2S device can issue a single i2strigger() call naming the unwired direction and cause a load from address 0 in kernel mode. Among the Espressif parts that carry this driver, userspace is available in-tree only on RISC-V SoCs with CONFIGRISCVPMP, and v4.4.0 is the first release where that is buildable: the ESP32-C6 HPCORE selects RISCVPMP when it is not built for MCUboot. ESP32-C5 in v4.4.x carries the same PMP-region and userspace linker support but does not select RISCVPMP by default. Espressif Xtensa targets do not support Zephyr userspace, and in a non-userspace build the bad direction can only come from in-kernel application code.
The impact is limited to availability: the access is a read at offset 0 of the missing stream structure, so there is no attacker-controlled offset, no write primitive and no information disclosure. With the default fatal-error handler the resulting exception halts the system, giving an unprivileged user-mode thread a system-wide denial of service. The fix adds the same pointer check the I2SDIRBOTH branch already performed and returns -ENOSYS for a direction the instance does not implement; the driver's other entry points (i2sesp32configcheck(), i2sesp32configget(), i2sesp32read(), i2sesp32write()) already guarded the pointers, and a static audit found no equivalent unguarded path.
The driver defect is older than the affected range. The unguarded dereference is present from v4.2.0 (reached through i2sesp32triggerstream(), whose if (stream) guard tests the address of a struct member and is never false) and takes its present i2sesp32triggercheck() form in v4.3.0. No in-tree Espressif configuration before v4.4.0 can run a user-mode thread, so in v4.2.x and v4.3.x the direction argument can only come from trusted kernel code. Those releases carry the bug but are not listed as affected; the fix has also been merged to v4.3-branch as hardening.
The NXP MCUX TRNG entropy driver in drivers/entropy/entropymcuxtrng.c passed the caller's byte count straight to the vendor SDK routine TRNGGetRandomData(). On i.MX RT5xx and RT6xx parts the SDK compiles its TRNGSWHEALTHTESTS variant, which always copies whole 32-bit words and draws entropy rounded up to a multiple of 128 bytes. Its "caller buffer is full" guard tests dataSize == 0, so a request whose length is not a multiple of four makes dataSize underflow past zero and the SDK keeps writing into the caller's buffer for the entire extraction: a 1-byte request results in 128 bytes written, and any non-word-multiple length overflows by up to 127 bytes.
entropygetentropy() is a syscall, and its verifier in drivers/entropy/entropyhandlers.c validates only the requested length via KSYSCALLMEMORYWRITE(). With CONFIGUSERSPACE enabled, an unprivileged user-mode thread that has merely been granted the entropy device can therefore choose both the destination address and a length such as 1, and cause the kernel to write up to 127 bytes beyond the region it proved it owns. On these Cortex-M33 targets there is no MMU, so user partitions and kernel data share one SRAM and the overflow can land in adjacent kernel state. The same defect is reached from kernel mode by any caller requesting a non-word-multiple length, including getentropy() and, in builds where syscsrandget()/sysrandget() resolve to the hardware generator, sysrand8get() and sysrand16get().
Impact is memory corruption of up to 127 bytes immediately following the supplied buffer — typically the caller's stack in kernel-mode use, or memory outside the caller's partition when driven through the syscall. The overflow offset is fully determined by the requested length and is therefore deterministic, while the written content is uncontrolled TRNG output; the practical consequences range from crashes and unpredictable state corruption to opportunistic escalation when kernel bookkeeping such as object permission bitmaps is overwritten. The affected devices are those where the MCUX SDK enables TRNGSWHEALTHTESTS (MIMXRT595S, MIMXRT555S, MIMXRT533S, MIMXRT685S, MIMXRT633S), on which the TRNG is the zephyr,entropy chosen node; other SoCs using this driver take the SDK path that clamps the copy size and are unaffected.
The fix routes any unaligned prefix and any sub-word tail through a local bounce word and hands the SDK only word-multiple sizes, so the SDK's word-granular writes can no longer pass the end of the caller's buffer.