---
id: "CVE-2026-99018"
kind: "vulnerability"
aliases: ["CVE-2026-99018"]
title: "Linux kernel RISC-V: unprivileged user can hijack kernel shadow call stack"
severity: "high"
cvss: null
epss: 0.00209
exploited: false
status: "open"
category: "runtime"
products: [{"name": "linux-kernel", "fixed": ["commit 4d0697f19297"], "affected": "RISC-V builds with CONFIG_SHADOW_CALL_STACK, from commit d1584d791a29 until 4d0697f19297; release versions unknown", "ecosystem": "linux"}]
ecosystems: ["linux"]
tags: ["kernel", "linux", "local-privilege-escalation", "riscv", "runtime", "self-hosted"]
not_affected_if: ["non-riscv", "shadow-call-stack-disabled"]
published_at: "2026-10-23T13:17:24+00:00"
first_seen_at: "2026-10-23T21:57:33+00:00"
updated_at: "2026-10-25T12:38:50+00:00"
sources: [{"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-99018", "first_seen_at": "2026-10-23T21:57:33+00:00"}, {"source": "analyst", "url": null, "first_seen_at": "2026-10-25T04:57:10+00:00"}, {"source": "epss", "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-99018", "first_seen_at": "2026-10-25T12:38:50+00:00"}]
---

# Linux kernel RISC-V: unprivileged user can hijack kernel shadow call stack

On RISC-V kernels built with shadow call stack, a user task can set its tp register to match its task_struct address so the kernel skips reloading the shadow call stack pointer. The kernel then runs with an attacker-controlled gp, giving an unprivileged arbitrary kernel write and control-flow hijack.

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

## Affected

- linux:linux-kernel affected RISC-V builds with CONFIG_SHADOW_CALL_STACK, from commit d1584d791a29 until 4d0697f19297; release versions unknown fixed commit 4d0697f19297

## How to check

Only RISC-V kernels are affected. Check the build config: grep CONFIG_SHADOW_CALL_STACK /boot/config-$(uname -r) (or zcat /proc/config.gz); '=y' means exposed. Check uname -m shows riscv64. Then confirm your distro kernel includes commit 4d0697f19297 (check the changelog); on a source tree, check that arch/riscv/kernel/entry.S uses scs_load_current_if_from_user s1, s5 rather than scs_load_current_if_task_changed.

## Fix

Update to a kernel release that contains upstream commit 4d0697f192975dab5985e921989be1d85cf580e9 ('riscv: entry: gate shadow call stack reload on sstatus.SPP, not tp'). Stable backports to 6.18 and 7.2 were in review; the exact released versions were not found, so check your distro or kernel.org changelog. Until then, build without CONFIG_SHADOW_CALL_STACK on RISC-V.

## Timeline

- 2026-10-23T21:57:33+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-99018
- 2026-10-25T04:57:10+00:00 analyst — Read kernel patch thread: fix is upstream commit 4d0697f19297 (riscv fixes tree, applied 2026-10-08, Fixes d1584d791a29 'riscv: Implement Shadow Call Stack'); 6.18 and 7.2 stable backports at review stage only. No released fixed version read, so left unknown. No PoC link or exploitation reports found.
- 2026-10-25T12:38:50+00:00 epss https://api.first.org/data/v1/epss?cve=CVE-2026-99018

## Documents

- [advisory] [PATCH] riscv: entry: gate shadow call stack reload on sstatus.SPP, not tp (thread: review, applied to riscv fixes) https://lore.landau.one/lkml/179142720833.3779518.9095154686634071421.git-patchwork-notify@kernel.org/T/
- [describes] d1584d7 riscv: Implement Shadow Call Stack (introducing commit) https://github.com/torvalds/linux/commit/d1584d791a297aa8ed93503382a682a6ecfc4218
- [fix] 6.18-stable review patch: commit 4d0697f19297 upstream https://ci.syzbot.org/patches/843257b7-dd98-4314-87b6-0394f4c429ed

## From the sources

In the Linux kernel, the following vulnerability has been resolved:

riscv: entry: gate shadow call stack reload on sstatus.SPP, not tp

Under CONFIG_SHADOW_CALL_STACK, handle_exception() reloads the kernel
shadow call stack pointer (gp) only when the trap came from userspace.
It made that decision with scs_load_current_if_task_changed, which
compares the trap-time tp (read back from sscratch into s5) against the
current tp:

	beq	\prev, tp, _skip_scs
	scs_load_current
_skip_scs:

tp is a user-writable register.  A user task that sets tp to the
linear-map address of its own task_struct (&current) before trapping
makes the comparison succeed (can achieve by brute force with KASLR
enable under our test), so the kernel skips scs_load_current and
keeps running with the gp that userspace left in place.  Under
CONFIG_SHADOW_CALL_STACK gp is the shadow call stack pointer and
load_global_pointer is a no-op, so the compiler-emitted
"sd ra, -8(gp)" / "ld ra, -8(gp)" in every non-leaf function then write
to, and return through, an attacker-controlled address -- an
unprivileged kernel arbitrary-write and control-flow-hijack primitive.
The poisoned gp is saved to task_struct.scs_sp on return, so the reload
is skipped on every subsequent entry as well.

Decide "came from userspace" from the hardware sstatus.SPP bit (already
in s1 at this point) instead of from tp, mirroring the check that
ret_from_exception already uses a few instructions later.  SPP is
written by hardware on trap entry and cannot be forged from U-mode.
Legitimate behaviour is unchanged: user entries (SPP=0) reload the task
shadow call stack, nested kernel entries (SPP=1) skip it.

---

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