Where
-Infinity
0
Severity
9.8
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
9.4
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H

dnsunpackname() caches the buffer tailroom once and reuses it while appending DNS labels. As the buffer grows, the cached size becomes incorrect, and the final null terminator can be written past the buffer. With assertions disabled (default), a malicious DNS response can trigger an out-of-bounds write when CONFIGDNSRESOLVER is enabled.

First published (updated )
Severity
8.8
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H

A remote, unauthenticated BLE peer can trigger a 2-byte out-of-bounds write in the Bluetooth host during L2CAP LE CoC SDU reassembly. When the application enables segmentation (via chanops.allocbuf) and the chosen RX pool has a userdatasize smaller than 2 bytes, the segmentation counter stored in the netbuf userdata area is written out of bounds in l2capchanlerecvseg (subsys/bluetooth/host/l2cap.c). The observed effects are an AddressSanitizer abort and, without ASan, heap corruption / fatal error.

First published (updated )
Severity
8.8
Use After Free
AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

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.

First published (updated )
Severity
8.8
AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
8.8
AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

The Bluetooth HCI USB transport class of the new USB device stack, subsys/usb/devicenext/class/bthci.c, copies every controller-to-host HCI packet into a USB transfer buffer before enqueuing it on an endpoint. bthcitxsyncin() allocated that buffer from bthcieppool, a fixed pool whose slots are only USBDMAXBULKMPS bytes (64 on full-speed-only builds), and then performed netbufaddmem(buf, btbuf->data, btbuf->len) with no length check. HCI event and ACL buffers are sized by CONFIGBTBUFEVTRXSIZE and CONFIGBTBUFACLRXSIZE and can be far larger, so the copy runs past the end of the 64-byte slot into the adjacent slots and into whatever follows the statically allocated pool.

The device runs the local Bluetooth controller in raw HCI mode (CONFIGBTHCIRAW), so the oversized packets originate over the air. With the HCI buffer sizes recommended for full-featured controllers, an unauthenticated attacker within Bluetooth range can supply the overflowing bytes: a connected peer that negotiates Data Length Extension can send ACL fragments up to CONFIGBTCTLRDATALENGTHMAX bytes, and an attacker that merely broadcasts extended advertising data reaches the same path through LE Extended Advertising Report events, which the controller fragments only to CONFIGBTBUFEVTRXSIZE. In the stock configuration the overflow is already reached without any radio traffic: the mandatory Read Supported Commands command-complete event is 70 bytes and overruns the 64-byte slot by six bytes on every host initialization.

Because netbufaddmem() validates tailroom only through an assertion unless CONFIGNETBUFHARDENING is enabled, builds with CONFIGASSERT=n — the usual production setting — silently corrupt memory adjacent to the USB buffer pool with attacker-chosen contents, which on a target without memory protection between subsystems can lead to arbitrary code execution. Builds with assertions enabled fail the assertion instead and panic, giving a remotely triggerable denial of service. The fix introduces a separate bthcitxpool sized to MAX(BTBUFACLRXSIZE, BTBUFEVTRXSIZE) for the controller-to-host direction while leaving the host-to-device pool at the endpoint packet size, which is already bounds-checked during reassembly.

First published (updated )
Severity
8.8
Use After Free
AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

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.

First published (updated )
Severity
8.8
Use After Free
AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

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.

First published (updated )
Severity
8.8
AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
8.8
Integer Overflow
AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

dynamicobjectcreate() in kernel/userspace/userspace.c computed the backing allocation for a dynamically allocated kernel object as objsizeget(otype) + size, and for thread stack elements as STACKELEMENTDATASIZE(size) (a round-up plus fixed overhead), without checking either expression for unsigned wrap-around. A size close to SIZEMAX makes the computed total wrap to a very small value, so the heap chunk handed out is a few bytes while the object descriptor is still tagged with the full requested type and registered in the kernel object table.

The size argument reaches that arithmetic directly from user mode. kobjectallocsize() is declared syscall in include/zephyr/sys/kobject.h, its verifier zvrfykobjectallocsize() in kernel/userspace/userspacehandler.c is a bare pass-through, and zobjectalloc() only range-checks otype — nothing bounds size. The stack-element branch is additionally reachable through the kthreadstackalloc() syscall via kernel/dynamic.c. Because subsequent kernel-object validation checks only the object's type and initialization state, the undersized handle passes KSYSCALLOBJINIT()/KSYSCALLOBJNEVERINIT(), and the matching init syscall (for example kmutexinit(), kseminit(), or kthreadcreate()) then writes a complete object over the truncated allocation.

An unprivileged user-mode thread can therefore trigger a supervisor-mode out-of-bounds write into the kernel resource-pool heap, of a size and content it substantially controls, corrupting sysheap chunk metadata and adjacent kernel objects. Under CONFIGGENPRIVSTACKS the thread-stack branch additionally stores an attacker-influenced wild pointer as a user thread's privileged stack base. The practical result is escape from the CONFIGUSERSPACE sandbox — kernel-level code execution or at minimum kernel memory corruption and system compromise.

Exploitation requires CONFIGUSERSPACE together with CONFIGDYNAMICOBJECTS (also selected by CONFIGDYNAMICTHREAD under userspace), and a calling thread with an assigned resource pool. The fix rejects both overflowing computations and frees the partially built descriptor.

First published (updated )
Severity
8.8
AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
8.8
AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

The flashcopy() system call is verified by zvrfyflashcopy() in drivers/flash/flashutil.c. On builds with CONFIGUSERSPACE enabled, this handler is the kernel-side trust boundary for a user-mode caller. Prior to the fix it validated only the output buffer (KSYSCALLMEMORYWRITE) and passed the two struct device arguments, srcdev and dstdev, directly into the implementation without any object validation — unlike every sibling flash syscall, which guards its device pointer with KSYSCALLDRIVERFLASH.

A user-mode thread fully controls the values of srcdev/dstdev and the contents of its own address space. The implementation zimplflashcopy() dereferences these pointers and calls through their driver-API function tables (e.g. api->getparameters(dstdev), flashread(srcdev, ...), flashwrite(dstdev, ...)). By supplying a pointer to a forged struct device whose api table contains attacker-chosen function pointers, an unprivileged thread can cause the kernel to call arbitrary code in supervisor mode; passing any arbitrary or invalid address otherwise yields a kernel crash or out-of-bounds read.

The result is a local privilege escalation out of the userspace sandbox (with kernel denial-of-service and information disclosure as lesser outcomes). The fix adds KSYSCALLDRIVERFLASH(srcdev, read) and KSYSCALLDRIVERFLASH(dstdev, write) to zvrfyflashcopy(), which verify each device is a registered flash-driver kernel object the calling thread is permitted to use before any dereference, closing the path completely.

First published (updated )
Severity
8.7
AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:H

Zephyr's IP socket recvmsg() implementation (subsys/net/lib/sockets/socketsinet.c, insertpktinfo()) validated the user-supplied ancillary (msgcontrol) buffer using only the payload length (msg->msgcontrollen < pktinfolen) before writing a full control message consisting of an aligned cmsg header plus the payload. Because the check omitted the cmsg header size, a control buffer whose length falls in the under-checked window (e.g. 16-27 bytes for IPv4 IPPKTINFO on a 64-bit target, where a single element actually occupies 28 bytes) passes the guard yet causes a fixed-size out-of-bounds write of up to one cmsg header (~12 bytes) past the end of the buffer.

Under CONFIGUSERSPACE the recvmsg verifier allocates a kernel-heap copy of the control buffer sized to msgcontrollen and runs the implementation against it, so the overflow corrupts kernel heap memory and is triggerable from an unprivileged userspace thread; in supervisor mode it corrupts the caller's buffer.

The path is reachable on a UDP/IP socket with IPPKTINFO/IPV6RECVPKTINFO (or hoplimit/timestamping) enabled when the application calls recvmsg() with an undersized control buffer and a datagram is received; part of the overwritten bytes (the destination IP in ipiaddr) is influenced by the received packet.

The fix makes the capacity check use NETCMSGSPACE(pktinfolen) (aligned header + aligned data) and returns -ENOMEM when the buffer is too small. Affected: v3.6.0 through v4.4.0.

First published (updated )
Severity
8.4
AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H

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.

First published (updated )
Severity
8.3
AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H

The Zephyr ADIN2111/ADIN1110 10BASE-T1S/T1L Ethernet driver (drivers/ethernet/ethadin2111.c) reassembles received Ethernet frames in OPEN Alliance (OA) SPI mode by copying device-supplied 64-byte data chunks into a fixed static buffer ctx->buf of size CONFIGETHADIN2111BUFFERSIZE (default 1524 bytes). In ethadin2111oadataread(), each valid chunk was memcpy'd into ctx->buf[ctx->scur] and the write cursor scur advanced, with no check that scur + len stayed within the buffer. The number of chunks (up to 255, from the BUFSTS RCA field) and the per-chunk length are taken entirely from the frame data received off the wire; the cursor is only reset on a start-of-frame chunk. An attacker on the single-pair Ethernet segment can therefore send a frame whose reassembled size exceeds the configured buffer, causing the driver's RX offload thread to write attacker-controlled frame bytes past the end of the static buffer into adjacent driver/kernel memory (up to roughly 14.8 KB in the worst case). This is a remotely/adjacently reachable out-of-bounds write (CWE-787) that can corrupt memory and cause denial of service or potentially code execution. The defect was introduced when OA SPI support was added (commit 0ca8b0756b1) and shipped in releases v3.7.0 through v4.4.0. The fix adds a bounds check that drops the oversized frame and resets the cursor before the copy.

First published (updated )
Severity
8.2
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H

The WireGuard VPN service in subsys/net/lib/wireguard/wg.c updated the peer endpoint from a message it could not authenticate. In handletransportdata(), when the 32-bit receiver index of an incoming transport-data message matched no session, the code both scheduled a new handshake and copied the datagram's source address into peer->endpoint with memcpy(&peer->endpoint, peeraddr, sizeof(peer->endpoint)). Because the receiver index is unknown, there is no key with which such a message could be verified, so the address written into peer->endpoint is entirely attacker-supplied. Every other endpoint update in the implementation happens only after authentication — after the Noise handshake in handlehandshakeinit(), or after AEAD decryption plus the anti-replay check in wgprocessdatamessage().

The WireGuard listener is registered with netudpregister() using no remote address or port filter and defaults to UDP port 51820 (CONFIGWIREGUARDPORT), and wgctrlrecv() resolves the peer by interface rather than by any cryptographic identity. Any host able to send a UDP datagram to that port can therefore reach the defect off-path, with no need to observe legitimate traffic: a 16-byte datagram carrying the transport-data type, zeroed reserved bytes and an arbitrary receiver index is sufficient. The vulnerable branch runs whenever the peer has neither a valid handshake nor a current keypair — the state at boot and after a session expires.

A single spoofed datagram redirects peer->endpoint to an address of the attacker's choosing, and all subsequent handshake initiations, keepalives and encrypted data packets are sent there. The legitimate peer never receives the initiations and so never re-authenticates, and nothing resets the endpoint afterwards, leaving the tunnel persistently unusable on devices whose peer does not initiate handshakes itself. The attacker can also relay the redirected initiations to the real peer to place itself in the path of the tunnel, observing ciphertext and traffic metadata and selectively dropping packets. WireGuard's end-to-end cryptography still prevents decryption or forgery of tunnel content, so the impact is loss of availability plus limited confidentiality, not key compromise.

The fix removes the endpoint learning from this path entirely: the stale-session recovery is kept, but the handshake is sent only to an endpoint learnt from an authenticated message, falling back to the configured cfgendpoint, and the message is dropped when neither is known. CONFIGWIREGUARD is marked experimental and is not enabled by default.

First published (updated )
Severity
8.2
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:L

subsys/net/lib/lwm2m/lwm2mpullcontext.c copied the firmware-update Package URI into a fixed static buffer (context.uri, size CONFIGLWM2MSWMGMTPACKAGEURILEN, default 128) with memcpy(context.uri, uri, LWM2MPACKAGEURILEN), copying exactly the destination size with no length validation. The Firmware-Update object stores the server-supplied Package URI (/5/0/1) in a 255-byte buffer, so a LwM2M management server (or an on-path attacker on a session lacking strong DTLS) can WRITE a URI of 128-254 characters; only the first 128 bytes are then copied into context.uri with no NUL terminator. That buffer is subsequently consumed as a C string by httpparserparseurl(context.uri, strlen(context.uri), ...), strlen-based CoAP URI-path/PROXY-URI option appends, and lwm2mparsepeerinfo(), causing an out-of-bounds read of adjacent static memory. The over-read bytes are appended to outbound CoAP requests (information disclosure of adjacent device memory to the server/proxy) and can crash the device (denial of service). The vulnerable copy was introduced by the pull-context refactor (first released in v3.0.0) and is present through v4.4.0; the default-on CONFIGLWM2MFIRMWAREUPDATEPULLSUPPORT path is affected. The fix adds a strlen(uri) >= sizeof(context.uri) check returning -ENOMEM and switches to strcpy(), guaranteeing a bounded, NUL-terminated buffer.

First published (updated )
Severity
8.1
Null Pointer Dereference
AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

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.

First published (updated )
Severity
8.1
Integer Underflow
AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

ieee802154decipherdataframe() in subsys/net/l2/ieee802154/ieee802154frame.c computed payloadlen = netpktgetlen(pkt) - llhdrlen - authtaglen without first checking that the received frame is at least llhdrlen + authtaglen bytes long. All three variables are uint8t, so a frame whose payload is shorter than the configured authentication tag makes the subtraction wrap around to a large value (up to 255).

The wrapped length is passed unchanged to ieee802154decryptauth() and on to the CCM operation as cipherpkt.inlen/outbufmax, with apkt->tag pointing at frame + llhdrlen + payloadlen. Because the receive buffer is allocated to the exact length of the frame received from the radio driver, the crypto layer then reads several hundred bytes past the end of the packet buffer and writes the same number of decrypted bytes back over it in place. The frame's authentication tag is only verified after this processing has taken place, so no key material, association or prior authentication is needed — a single crafted short frame from any device in radio range is sufficient. Frame validation in ieee802154validateframe() does not prevent it: a data frame is accepted with a one-byte payload.

The result is an out-of-bounds read and an out-of-bounds write of up to roughly 240 bytes into the adjacent network-buffer pool, corrupting other packets or allocator metadata and typically faulting the target. The out-of-bounds content is not attacker-chosen (it is ciphertext XOR keystream over out-of-bounds memory) and the frame is dropped when tag verification fails, so the primary impact is memory corruption and denial of service rather than information disclosure.

Exposure is limited to configurations that enable the experimental CONFIGNETL2IEEE802154SECURITY option, select a crypto device via CONFIGNETL2IEEE802154SECURITYCRYPTODEVNAME, and have established a security session with a level other than IEEE802154SECURITYLEVELNONE; with security disabled or at level NONE the tag length is zero and no underflow occurs. The fix rejects frames shorter than llhdrlen + authtaglen before the subtraction, and adds the matching guard on the transmit side in ieee802154createdataframe().

First published (updated )
Severity
8.1
AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

The IPv6 neighbor-discovery code in subsys/net/ip/ipv6nbr.c processes the 6LoWPAN Context Option (6CO, RFC 6775) carried inside ICMPv6 Router Advertisements. In handlera6co() the 8-bit contextlen field is taken directly from the packet and was never bounded to the RFC maximum of 128. The function computes context->contextlen / 8 and then performs memset(context->prefix + contextlen, 0, sizeof(context->prefix) - contextlen), where context->prefix is a fixed 16-byte array.

With contextlen between 136 and 255 (and the option length field set to 3, which the pre-fix validation accepts), contextlen / 8 evaluates to 17..31, so the memset length 16 - contextlen/8 underflows the unsigned sizet argument to roughly SIZEMAX. This produces an unbounded out-of-bounds memset that zeroes kernel memory well past the 6lo context structure.

The defect is reachable from unauthenticated, link-local input: any host on the same link can send a crafted Router Advertisement with a 6CO option. The RA handler validates only the option length field before calling handlera6co(), so a single packet triggers the wild write. The code is compiled when CONFIGNET6LOCONTEXT is enabled.

The impact is a reliable remote (adjacent) denial of service via memory corruption, with collateral integrity loss as the memset zeroes contiguous memory before the system faults. Router Advertisements are link-scoped and not forwarded, so the attacker must be on the same link (AV:A). The fix rejects any contextlen greater than 128 before the length computation.

First published (updated )
Severity
8.1
AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

The IPv6 Neighbor Discovery handlers in subsys/net/ip/ipv6nbr.c (handlerainput, handlensinput, handlenainput) used an incorrect boolean expression that combined the RFC 4861 validity checks with the ICMPv6 code check using the wrong operator precedence: the form was ((length/hop/source/target checks) && (icmphdr->code != 0)). Because every legitimate ND message carries ICMPv6 code 0, an attacker setting code == 0 (the normal value) caused the entire predicate to evaluate false, so the packet was never dropped and all of the other checks were silently skipped. The bypassed checks include the mandatory Hop Limit == 255 verification (which proves an ND packet originated on-link and was not forwarded) and, for Router Advertisements, the requirement that the source be a link-local address, as well as multicast-target sanity checks. As a result, an adjacent on-link attacker — and, because the Hop-Limit-255 guard is bypassed, potentially a remote/off-link attacker whose packets would otherwise be rejected — can have forged Router Advertisement, Neighbor Solicitation, and Neighbor Advertisement messages accepted. A forged RA lets the attacker reconfigure the victim's default router, on-link prefixes (SLAAC), MTU, reachable/retransmit timers, and (with CONFIGNETIPV6RARDNSS) DNS servers, while forged NS/NA enable neighbor-cache poisoning, enabling man-in-the-middle, traffic redirection, and denial of service. The flaw is an input-validation/authentication weakness rather than a memory-safety issue: the underlying packet-parsing primitives (netpktgetdata, netpktread, netpktskip) are independently bounds-safe and the validated length is the true buffer length, so skipping the length check causes no out-of-bounds access. The defect has existed since the logic was introduced in 2018 and shipped in all releases through v4.4.0; it is fixed by splitting the condition so any failing check drops the packet.

First published (updated )
Severity
7.8
Use After Free
AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

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.

First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
7.8
Use After Free
AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

The arm64 MMU backend invalidates a single virtual address from the TLB with invalidatetlbpage() in arch/arm64/core/mmu.c, which issued TLBI VAE1{IS} with an operand of virt >> 12. That operand encodes the ASID in bits [63:48], so the invalidation only ever matched ASID 0 (the kernel page tables) and global entries. Memory domains, however, are allocated ASIDs starting at 1 in archmemdomaininit(), and every domain-private leaf created by privatemap() is mapped with MTNG, i.e. non-global. A domain's cached translation therefore survived the invalidation.

All callers of invalidatetlbpage() are demand-paging paths. When the eviction algorithm reclaims a frame, pageframepreparelocked() in kernel/mmu.c calls archmempageout(), which marks the kernel leaf invalid, propagates the invalid descriptor into every domain via syncdomains(), and then flushes only ASID 0. dopagefault() immediately reuses the very same page frame for the incoming mapping through archmempagein(). A user thread that had already touched the evicted page — a kmempartition added with archmemdomainpartitionadd(), or its own stack mapped by mapthreadstack() — retains a valid EL0 read/write translation to that physical frame under its own ASID.

Unprivileged code running in a user thread can drive memory pressure to force eviction and then keep accessing the stale address, obtaining read and write access to whatever the kernel subsequently placed in that frame: supervisor data, another memory domain's partition, or another thread's stack. This defeats the isolation that CONFIGUSERSPACE is meant to enforce, allowing information disclosure and memory corruption up to full compromise of the system.

The fix switches the implementation to TLBI VAAE1{IS}, which matches every ASID and both global and non-global entries, so no domain-tagged translation survives a page out, dirty-bit update or access-flag clear. Exploitation requires an arm64 build with both CONFIGUSERSPACE and CONFIGDEMANDPAGING enabled, an active backing store, and at least one demand-paged page that is also privately mapped into a user memory domain. The same commit additionally stops globalizetable() from stripping EL0 permission and non-global attributes off domain-private leaves during paging synchronization, which previously caused user threads to lose access to their own partitions and stacks.

1 / 2
Source: MITRE
First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

The GPIO syscall handlers in drivers/gpio/gpiohandlers.c did not validate the pin argument supplied by a user-mode caller. zvrfygpiopinconfigure(), zvrfygpiopingetconfig() and zvrfygpiopininterruptconfigure() verified only the device object (KSYSCALLDRIVERGPIO() / KSYSCALLOBJ()) and then passed the caller-supplied gpiopint straight through to zimplgpiopin() and on to the driver. The only pin check in include/zephyr/drivers/gpio.h was ASSERT(gpioportpinissupported(cfg->portpinmask, pin), ...), which compiles to nothing unless CONFIGASSERT is enabled, so in a production build no check existed at all.

GPIO drivers use pin as a shift count and as an array index on the documented assumption that it is a pin the controller supports. On STM32, for example, gpiostm32pininterruptconfigure() turns pin into an EXTI line and stm32gpiointcsetirqcallback() writes data->cb[linenum].cb and data->cb[linenum].data into a NUMEXTILINES-entry array (default 16) in drivers/interruptcontroller/intcgpiostm32.c, guarded only by a compiled-out ASSERTNOMSG(); the same function then reads extiirqtable[linenum] and calls irqenable() on the result. Other controllers index fixed-size per-pin configuration tables the same way — drivers/gpio/gpioiteit8xxx2v2.c reads an MMIO base pointer out of wucbase[8] with the caller's pin and performs a read-modify-write through it.

The issue is reachable only in builds with CONFIGUSERSPACE=y (the file is compiled under that symbol), by a user-mode thread that has been granted access to a GPIO device object. Such a grant is intended to authorize control of that port's pins; the missing validation instead lets the thread drive a supervisor-mode out-of-bounds write of a function pointer and an associated void past a kernel .bss array, and out-of-bounds reads that feed irqenable() and MMIO accesses — an escape from the user-mode sandbox that can lead to kernel memory corruption, arbitrary code execution, or a crash.

The fix validates pin against the controller's gpiodriverconfig.portpinmask with gpioportpinissupported() in every pin-taking zvrfy handler and returns -EINVAL otherwise, complemented by an unconditional pin >= GPIOMAXPINSPERPORT range check added to the shared zimplgpiopin() inlines.

First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

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.

First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

The GPIO syscall handlers in drivers/gpio/gpiohandlers.c did not validate the pin argument supplied by a user-mode caller. zvrfygpiopinconfigure(), zvrfygpiopingetconfig() and zvrfygpiopininterruptconfigure() verified only the device object (KSYSCALLDRIVERGPIO() / KSYSCALLOBJ()) and then passed the caller-supplied gpiopint straight through to zimplgpiopin() and on to the driver. The only pin check in include/zephyr/drivers/gpio.h was ASSERT(gpioportpinissupported(cfg->portpinmask, pin), ...), which compiles to nothing unless CONFIGASSERT is enabled, so in a production build no check existed at all.

GPIO drivers use pin as a shift count and as an array index on the documented assumption that it is a pin the controller supports. On STM32, for example, gpiostm32pininterruptconfigure() turns pin into an EXTI line and stm32gpiointcsetirqcallback() writes data->cb[linenum].cb and data->cb[linenum].data into a NUMEXTILINES-entry array (default 16) in drivers/interruptcontroller/intcgpiostm32.c, guarded only by a compiled-out ASSERTNOMSG(); the same function then reads extiirqtable[linenum] and calls irqenable() on the result. Other controllers index fixed-size per-pin configuration tables the same way — drivers/gpio/gpioiteit8xxx2v2.c reads an MMIO base pointer out of wucbase[8] with the caller's pin and performs a read-modify-write through it.

The issue is reachable only in builds with CONFIGUSERSPACE=y (the file is compiled under that symbol), by a user-mode thread that has been granted access to a GPIO device object. Such a grant is intended to authorize control of that port's pins; the missing validation instead lets the thread drive a supervisor-mode out-of-bounds write of a function pointer and an associated void past a kernel .bss array, and out-of-bounds reads that feed irqenable() and MMIO accesses — an escape from the user-mode sandbox that can lead to kernel memory corruption, arbitrary code execution, or a crash.

The fix validates pin against the controller's gpiodriverconfig.portpinmask with gpioportpinissupported() in every pin-taking zvrfy handler and returns -EINVAL otherwise, complemented by an unconditional pin >= GPIOMAXPINSPERPORT range check added to the shared zimplgpiopin() inlines.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203