// SPDX-License-Identifier: GPL-2.0 /* * cgcap.ko — Shannon SIT/RCM frame capture (read-only) * * Kprobes cpif's ipc_write() — the .write of the FMT/RAW char devices * (/dev/umts_ipc0 ch 0xf5, /dev/oem_ipc0 ch 0x81, ...). rild/libsitril * write the BARE SIT frame there and cpif prepends the SIPC5 link header * in-kernel, so the buffer handed to ipc_write IS the exact SIT payload. * * We copy that payload out (nofault, atomic-safe), tag it with the io_device * channel id, and expose a decoded ring at /proc/cgcap/frames. This lets us * learn the real on-wire command bytes for setAllowedNetworkType / band-lock / * null-cipher by triggering those operations from Android and reading what * rild actually sent — no transmission, no guessing. * * SIT/RCM header (LE), proven from vendor.radio.protocol.sit.base.so: * off0 u8 type 0=REQ 1=RESP 2=IND * off2 u16 id (hi=group/main, lo=sub-cmd) * off4 u16 len total frame length incl header * off6 u32 token transaction id (REQ/RESP only) * off10 u8 error (RESP only) * off12 params * io_device->ch lives at struct offset 0x114 (proven from cpif_probe). */ #include #include #include #include #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("snake"); MODULE_DESCRIPTION("Shannon SIT/RCM frame capture via cpif ipc_write kprobe"); MODULE_VERSION("1.0"); #define IOD_CH_OFFSET 0x114 /* io_device->ch (u8/u16), cpif_probe RE */ #define CAP_DATA 160 /* bytes of payload kept per frame */ #define CAP_RING 512 /* ring capacity */ struct cap_ent { u64 seq; u64 ns; u32 ch; u32 len; /* full byte count of the transfer */ u32 n; /* bytes actually captured */ u8 dir; /* 0 = TX (ipc_write), 1 = RX (ipc_read) */ u8 data[CAP_DATA]; }; static struct cap_ent ring[CAP_RING]; static u32 ring_head; static u64 ring_seq; static DEFINE_SPINLOCK(ring_lock); /* Optional channel filter: 0 = capture all; else only this iod->ch. */ static int cap_ch = 0; module_param(cap_ch, int, 0644); MODULE_PARM_DESC(cap_ch, "capture only this cpif channel id (0=all)"); /* nofault readers, resolved via kallsyms (may be GKI-protected). */ static long (*p_cfu_nofault)(void *dst, const void __user *src, size_t size); static long (*p_cfk_nofault)(void *dst, const void *src, size_t size); static unsigned long cg_lookup_name(const char *name) { static unsigned long (*kln)(const char *name); if (!kln) { struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" }; int ret = register_kprobe(&kp); if (ret < 0) { pr_err("cgcap: kprobe bootstrap failed (%d)\n", ret); return 0; } kln = (void *)kp.addr; unregister_kprobe(&kp); } return kln ? kln(name) : 0; } /* iod = file->private_data; iod->ch @0x114 (proven from cpif_probe). */ static u32 iod_ch(struct file *f) { u16 v; if (f && f->private_data && !get_kernel_nofault(v, (u16 *)((char *)f->private_data + IOD_CH_OFFSET))) return v; return 0xffffffff; } static void store_frame(u8 dir, u32 ch, size_t count, const void __user *ubuf) { unsigned long flags; struct cap_ent *e; u32 n = min_t(u32, (u32)count, CAP_DATA); if (cap_ch && ch != (u32)cap_ch) return; spin_lock_irqsave(&ring_lock, flags); e = &ring[ring_head]; e->seq = ++ring_seq; e->ns = ktime_get_ns(); e->ch = ch; e->len = (u32)count; e->dir = dir; e->n = n; if (!p_cfu_nofault || p_cfu_nofault(e->data, ubuf, n)) e->n = 0; ring_head = (ring_head + 1) % CAP_RING; spin_unlock_irqrestore(&ring_lock, flags); } /* TX: ipc_write(file, user_buf, count, pos): arm64 x0=f, x1=buf, x2=count. */ static int cap_pre(struct kprobe *p, struct pt_regs *regs) { struct file *f = (struct file *)regs->regs[0]; const void __user *ubuf = (const void __user *)regs->regs[1]; size_t count = (size_t)regs->regs[2]; if (f && ubuf && count) store_frame(0, iod_ch(f), count, ubuf); return 0; } static struct kprobe kp_ipc_write = { .symbol_name = "ipc_write", .pre_handler = cap_pre, }; /* Store from a KERNEL buffer (skb->data), read as system — no user copy. */ static void store_kframe(u8 dir, u32 ch, const void *kbuf, u32 len) { unsigned long flags; struct cap_ent *e; u32 n = min_t(u32, len, CAP_DATA); if (cap_ch && ch != (u32)cap_ch) return; spin_lock_irqsave(&ring_lock, flags); e = &ring[ring_head]; e->seq = ++ring_seq; e->ns = ktime_get_ns(); e->ch = ch; e->len = len; e->dir = dir; e->n = n; if (!p_cfk_nofault || p_cfk_nofault(e->data, kbuf, n)) e->n = 0; ring_head = (ring_head + 1) % CAP_RING; spin_unlock_irqrestore(&ring_lock, flags); } /* RX: io_dev_recv_skb_single_from_link_dev(x0 = mld, x1 = iod, x2 = skb) — * the common CP->AP entry for BOTH FMT and RAW channels, skb in kernel memory. * Read skb->data directly (as root/system), no copy_from_user. */ static int rx_pre(struct kprobe *p, struct pt_regs *regs) { void *iod = (void *)regs->regs[1]; struct sk_buff *skb = (struct sk_buff *)regs->regs[2]; u16 v; u32 ch = 0xffffffff; if (!skb) return 0; if (iod && !get_kernel_nofault(v, (u16 *)((char *)iod + IOD_CH_OFFSET))) ch = v; store_kframe(1, ch, skb->data, skb->len); return 0; } static struct kprobe kp_rx = { .symbol_name = "io_dev_recv_skb_single_from_link_dev", .pre_handler = rx_pre, }; /* ---- /proc/cgcap/frames ---- */ static const char *sit_type(u8 t) { switch (t) { case 0: return "REQ"; case 1: return "RSP"; case 2: return "IND"; default: return "???"; } } static int frames_show(struct seq_file *m, void *v) { static struct cap_ent snap[CAP_RING]; unsigned long flags; u32 i, cnt; spin_lock_irqsave(&ring_lock, flags); memcpy(snap, ring, sizeof(snap)); cnt = ring_head; /* not strictly needed; we sort by seq */ (void)cnt; spin_unlock_irqrestore(&ring_lock, flags); seq_printf(m, "# total=%llu fields: seq dir ch type id(grp/cmd) len token err payload\n", ring_seq); for (i = 0; i < CAP_RING; i++) { struct cap_ent *e = &snap[i]; u8 type, err = 0; u16 id = 0, flen = 0; u32 tok = 0, j; if (!e->seq) continue; if (e->n == 0) { seq_printf(m, "%llu %s ch=0x%02x len=%u [nocopy]\n", e->seq, e->dir ? "RX" : "TX", e->ch, e->len); continue; } type = e->data[0]; if (e->n >= 6) { id = e->data[2] | (e->data[3] << 8); flen = e->data[4] | (e->data[5] << 8); } if (e->n >= 10) tok = e->data[6] | (e->data[7] << 8) | (e->data[8] << 16) | (e->data[9] << 24); if (type == 1 && e->n >= 11) err = e->data[10]; seq_printf(m, "%llu %s ch=0x%02x %s id=0x%04x(%02x/%02x) len=%u tok=%u err=%u ", e->seq, e->dir ? "RX" : "TX", e->ch, sit_type(type), id, (id >> 8) & 0xff, id & 0xff, flen, tok, err); for (j = 0; j < e->n; j++) seq_printf(m, "%02x", e->data[j]); if (e->len > e->n) seq_printf(m, "..(+%u)", e->len - e->n); seq_putc(m, '\n'); } return 0; } static int frames_open(struct inode *i, struct file *f) { return single_open(f, frames_show, NULL); } static const struct proc_ops frames_pops = { .proc_open = frames_open, .proc_read = seq_read, .proc_lseek = seq_lseek, .proc_release = single_release, }; static struct proc_dir_entry *proc_root; static int __init cgcap_init(void) { int ret; p_cfu_nofault = (void *)cg_lookup_name("copy_from_user_nofault"); p_cfk_nofault = (void *)cg_lookup_name("copy_from_kernel_nofault"); if (!p_cfu_nofault || !p_cfk_nofault) pr_warn("cgcap: nofault reader unresolved (u=%px k=%px); some payloads empty\n", p_cfu_nofault, p_cfk_nofault); ret = register_kprobe(&kp_ipc_write); if (ret < 0) { pr_err("cgcap: register_kprobe(ipc_write) failed (%d) — is cpif loaded?\n", ret); return ret; } ret = register_kprobe(&kp_rx); if (ret < 0) pr_warn("cgcap: register_kprobe(queue_skb_to_iod) failed (%d); TX-only\n", ret); proc_root = proc_mkdir("cgcap", NULL); if (proc_root) proc_create("frames", 0444, proc_root, &frames_pops); pr_info("cgcap: capturing cpif ipc_write @ %p (filter ch=0x%x); read /proc/cgcap/frames\n", kp_ipc_write.addr, cap_ch); return 0; } static void __exit cgcap_exit(void) { unregister_kprobe(&kp_ipc_write); unregister_kprobe(&kp_rx); if (proc_root) { remove_proc_entry("frames", proc_root); proc_remove(proc_root); } pr_info("cgcap: unloaded (%llu frames seen)\n", ring_seq); } module_init(cgcap_init); module_exit(cgcap_exit);