[PATCH] ceph: clamp MDS reply result code to the errno range

From: Yuanfu Xie

Date: Sat Sep 26 2026 - 07:58:34 EST


ceph_mdsc_wait_request() returns the reply head result code of the MDS
request to its callers as a plain int errno. The value is a
server-controlled u32 and nothing checks that it actually is an errno;
several callers pass it straight to ERR_PTR(), e.g.
ceph_finish_lookup() on the atomic open path:

err = ceph_mdsc_wait_request(...) /* e.g. +0x50fc00 */
...
if (err)
dentry = ERR_PTR(err);

IS_ERR() only recognizes the [-MAX_ERRNO, -1] window, so an
out-of-range value is not caught anywhere and survives as a "struct
dentry *". On the open path the positive error travels
ceph_finish_lookup() -> ceph_atomic_open() -> ERR_PTR() in
fs/namei.c's atomic_open() wrapper -> step_into(), which dereferences
it as a dentry and oopses (mainline 7.3-rc3-135-g4982d3552a3b, the
value in R12 is the server-supplied result):

BUG: KASAN: probably user-memory-access in range
[0x000000000050fc00-0x000000000050fc07]
RIP: 0010:step_into_slowpath+0x3e3/0xe60
R12: 000000000050fc00
Call Trace:
<TASK>
? __pfx_ceph_atomic_open+0x10/0x10
path_openat+0xf41/0x2440
do_file_open+0x228/0x480
do_sys_openat2+0x103/0x1d0
__x64_sys_openat+0x141/0x200
do_syscall_64+0xe8/0x4f0
entry_SYSCALL_64_after_hwframe+0x77/0x79
</TASK>

(0xdffffc00000a1f80 in the full report is the KASAN shadow of 0x50fc00;
without KASAN the raw value is read.) Values with the top bit set fare
no better: 0xd8000000 sign-extends through the int conversion into the
kernel address range and takes a page fault instead.

Clamp the result at the single read and return -EIO for anything
outside the errno range, which is what a malformed reply means there.
net/9p does the same to server errno values with safe_errno(). This
is the path the reported crash takes; the other readers of
head->result, including the ceph_async_create_cb() and
ceph_async_unlink_cb() callbacks, are not covered by this patch.

Each of the six crashing opens was run on a build of
7.3-rc3-135-g4982d3552a3bf with only this change applied: it crashes
the unpatched kernel on the first openat() (verified on the same
tree without the patch), and with the patch the open fails cleanly
without an oops. A few hundred normal file operations on the
patched kernel also completed without a crash; valid errno values
pass through unchanged.

Fixes: 2f2dc053404febedc9c273452d9d518fb31fde72 ("ceph: MDS client")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Yuanfu Xie <yuanfuxie@xxxxxxxxxxxxxx>

---
fs/ceph/mds_client.c | 8 ++++++++
1 file changed, 8 insertions(+)

diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
index 085ae0cfb5f7..3598099b1978 100644
--- a/fs/ceph/mds_client.c
+++ b/fs/ceph/mds_client.c
@@ -3985,6 +3985,14 @@
/* only abort if we didn't race with a real reply */
if (test_bit(CEPH_MDS_R_GOT_RESULT, &req->r_req_flags)) {
err = le32_to_cpu(req->r_reply_info.head->result);
+ if (err > 0 || err < -MAX_ERRNO) {
+ /*
+ * The result is server-controlled. Values outside
+ * the errno range are turned into bogus pointers by
+ * the ERR_PTR() users among the callers.
+ */
+ err = -EIO;
+ }
} else if (err < 0) {
doutc(cl, "aborted request %lld with %d\n", req->r_tid, err);