---
id: "CVE-2026-87038"
kind: "vulnerability"
aliases: ["CVE-2026-87038"]
title: "Zephyr: I3C syscall race lets a user thread run kernel code"
severity: "high"
cvss: 7.0
epss: null
exploited: false
status: "open"
category: "runtime"
products: [{"name": "zephyr", "fixed": ["4.3.2", "4.5.0"], "affected": ">= 3.2.0, < 4.3.2; >= 4.4.0, < 4.5.0", "ecosystem": "vendor"}]
ecosystems: []
tags: ["embedded", "i3c", "runtime", "userspace", "zephyr"]
not_affected_if: ["no-i3c-driver", "no-userspace"]
published_at: "2026-11-04T06:17:06+00:00"
first_seen_at: "2026-11-04T06:25:20+00:00"
updated_at: "2026-11-04T06:26:41+00:00"
sources: [{"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-87038", "first_seen_at": "2026-11-04T06:25:20+00:00"}, {"source": "analyst", "url": null, "first_seen_at": "2026-11-04T06:26:41+00:00"}]
---

# Zephyr: I3C syscall race lets a user thread run kernel code

The Zephyr I3C transfer system call checks the caller-supplied device descriptor in user memory, then reads the bus field again when it is used, so a second thread can swap it in between. A user thread can then make the kernel call an attacker-chosen function table in supervisor mode, giving kernel-mode code execution.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- vendor:zephyr affected >= 3.2.0, < 4.3.2; >= 4.4.0, < 4.5.0 fixed 4.3.2, 4.5.0

## How to check

- Run your Zephyr version (west list zephyr, or the VERSION file): affected if >= 3.2.0 and < 4.3.2, or >= 4.4.0 and < 4.5.0.
- Check the build config for CONFIG_USERSPACE=y and an I3C controller driver (CONFIG_I3C); without either you are unaffected.
- In drivers/i3c/i3c_handlers.c, z_vrfy_i3c_transfer() is fixed if it copies the descriptor with k_usermode_from_copy(); it is vulnerable if it only calls K_SYSCALL_MEMORY_READ(target) and K_SYSCALL_OBJ(target->bus).

## Fix

Upgrade to Zephyr 4.3.2 (3.2.0–4.3.1 line) or 4.5.0 (4.4.x line). Backports to v3.7-branch, v4.3-branch and v4.4-branch are merged, but the advisory lists no 3.7.x or 4.4.x patched release; on those lines, build from the branch or cherry-pick commit 550437bc9efd.

## Timeline

- 2026-11-04T06:25:20+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-87038
- 2026-11-04T06:26:41+00:00 analyst — Read Zephyr GHSA-86xj-2q6x-h82w and fix commit 550437bc. Affected >=3.2.0,<4.3.2 and >=4.4.0,<4.5.0; patched 4.3.2, 4.5.0. No exploitation or PoC seen; local, high complexity (CVSS 7.0). Did not search social posts.

## Documents

- [advisory] Vendor advisory: github.com https://github.com/zephyrproject-rtos/zephyr/security/advisories/GHSA-86xj-2q6x-h82w
- [fix] Patch: github.com https://github.com/zephyrproject-rtos/zephyr/commit/550437bc9efd7b733f13b9be73e77fd98858c8a2

## From the sources

The user-mode verification handler for the i3c_transfer() system call, z_vrfy_i3c_transfer() in drivers/i3c/i3c_handlers.c, validated the caller-supplied struct i3c_device_desc in place instead of snapshotting it. It confirmed only that the descriptor was readable by the calling thread (K_SYSCALL_MEMORY_READ()), read target->bus once to check it with K_SYSCALL_OBJ(), and then passed the caller's live pointer down. The implementation z_impl_i3c_transfer() re-reads target->bus from that user memory and dispatches through it as DEVICE_API_GET(i3c, target->bus)->i3c_xfers(...), so the bus that was validated is not necessarily the bus that is used. DEVICE_API_GET() performs no runtime type check outside CONFIG_DEVICE_API_ASSERT builds.

On a build with CONFIG_USERSPACE and an I3C controller driver, a user thread that has been granted an I3C device object can exploit this with a second thread in the same memory domain: one thread calls i3c_transfer() while the other rewrites the bus field of the descriptor. The window between the check and the use spans the num_msgs validation, the message-array access check, a stack VLA allocation and a memcpy() of up to 32 struct i3c_msg, so the race is repeatable and can simply be retried until it lands.

When the substitution succeeds, the kernel dereferences an attacker-chosen pointer as a struct device, loads a function pointer from the attacker-controlled api table and calls it in supervisor mode with attacker-influenced arguments. This yields kernel-mode code execution from an unprivileged user thread — a complete escape of the userspace memory-protection sandbox; lesser outcomes are an arbitrary two-level kernel read or a kernel fault. Builds without CONFIG_USERSPACE, or without an I3C driver, are unaffected.

The fix copies the descriptor into a kernel-stack local with k_usermode_from_copy(), validates the bus in that copy, and dispatches on the copy, so the validated value is provably the value used.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
