diff --git a/common/kmod/Makefile b/common/kmod/Makefile index b9ae767..dae02b9 100644 --- a/common/kmod/Makefile +++ b/common/kmod/Makefile @@ -1,16 +1,35 @@ # RadioControl out-of-tree kernel modules -# Build: make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL_DIR=/path/to/kernel +# +# Build all: +# make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL_DIR=/path/to/kernel +# +# Build specific module: +# make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL_DIR=/path/to/kernel MOD=rc_wifi_mon +# +# Target: Pixel 10 Pro Fold (rango), Tensor G5, kernel 6.6.102 KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build obj-$(CONFIG_RC_WIFI_MON) += rc_wifi_mon.o -obj-$(CONFIG_RC_DIAG_BRIDGE) += rc_diag_bridge.o obj-$(CONFIG_RC_SHANNON_CMD) += rc_shannon_cmd.o +obj-$(CONFIG_RC_DIAG_BRIDGE) += rc_diag_bridge.o # Default: build all CONFIG_RC_WIFI_MON ?= m -CONFIG_RC_DIAG_BRIDGE ?= m CONFIG_RC_SHANNON_CMD ?= m +CONFIG_RC_DIAG_BRIDGE ?= m + +# Allow building a single module via MOD=name +ifdef MOD + CONFIG_RC_WIFI_MON = n + CONFIG_RC_SHANNON_CMD = n + CONFIG_RC_DIAG_BRIDGE = n + CONFIG_$(shell echo $(MOD) | tr a-z A-Z) = m +endif + +# Extra compiler flags for Android/ARM64 +ccflags-y += -Wno-unused-function +ccflags-y += -DCONFIG_RADIOCONTROL all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules @@ -20,3 +39,15 @@ clean: install: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules_install + +# Copy built modules to the module package directory +package: + @mkdir -p ../../common/kmod_out + @for ko in *.ko; do \ + if [ -f "$$ko" ]; then \ + cp "$$ko" ../../common/kmod_out/; \ + echo "Packaged: $$ko"; \ + fi; \ + done + +.PHONY: all clean install package diff --git a/common/kmod/README.md b/common/kmod/README.md index 3bc7075..b81efb2 100644 --- a/common/kmod/README.md +++ b/common/kmod/README.md @@ -3,11 +3,13 @@ Out-of-tree kernel modules for enabling hardware features that are compiled out of production Android kernels. +Target: Pixel 10 Pro Fold (rango), Tensor G5, kernel 6.6.102 + ## Build Requirements -- Matching kernel headers for target device +- Matching kernel headers for target device (kernel 6.6.x) - ARM64 cross-compiler (aarch64-linux-gnu-gcc) -- Device-specific kernel config +- Device-specific kernel config (CONFIG_KPROBES=y, CONFIG_MODULES=y) ## Modules @@ -16,22 +18,55 @@ Patches the WiFi driver's nl80211 ops table at runtime to allow monitor mode and packet injection on chipsets that have the capability but disable it in their cfg80211 change_virtual_intf handler. -Supports: -- Samsung SCSC/SLSI (slsi_set_monitor_mode — re-enables compiled-out path) -- Broadcom bcmdhd (patches cfg80211_ops to allow NL80211_IFTYPE_MONITOR) -- Qualcomm ath11k/ath12k/cnss (typically already supports monitor, but - this bypasses vendor restrictions) +Features: +- Uses kprobes-based kallsyms lookup (works on kernel 5.7+) +- Patches wiphy->interface_modes bitmask for monitor + OCB +- Driver-specific firmware iovars for BCM4390 (monitor, promisc, allmulti) +- SCSC/SLSI MIB patching for Maxwell firmware monitor enable +- sysfs status at /sys/kernel/rc_wifi_mon/status +- Clean restore on module unload -### rc_diag_bridge.ko -Creates /dev/rc_diag — a simplified userspace interface to the Qualcomm -DIAG subsystem that bypasses the standard diag driver's filtering. -Allows reading/writing NV items and sending FTM commands from userspace. +Supports: +- Broadcom bcmdhd4390 (BCM4390, primary target) +- Samsung SCSC/SLSI (Exynos WiFi) +- Qualcomm ath11k/ath12k/cnss ### rc_shannon_cmd.ko Creates /dev/rc_shannon — direct command interface to Samsung Shannon modem bypassing RIL. Allows raw AT command passthrough and IPC message injection for band locking, NR mode control, and diagnostic readout. +Features: +- Auto-detects modem path (umts_atc0, nr_atc0, umts_router) +- URC (unsolicited result code) buffering with ring buffer (64 entries) +- Structured ioctl interface (RC_SHANNON_AT_CMD) with configurable timeout +- Simple read/write interface for basic use +- Statistics tracking (cmds sent, bytes tx/rx) +- Modem connectivity test on load +- Kernel 6.4+ class_create compatibility + +### rc_diag_bridge.ko +Creates /dev/rc_diag — a simplified userspace interface to the Qualcomm +DIAG subsystem. Handles HDLC framing internally. + +Features: +- NV item read/write (DIAG_NV_READ_F / DIAG_NV_WRITE_F) +- FTM commands (Factory Test Mode) via subsystem dispatch +- EFS2 file operations (open, read, write, stat, unlink) +- Full HDLC encode/decode with CRC-16 CCITT validation +- Modem version query +- Raw DIAG passthrough for advanced use +- Graceful inactive mode when no Qualcomm modem present + +Note: This module is for Qualcomm-baseband devices. On Tensor G5 with +Shannon 5400, use rc_shannon_cmd instead. rc_diag_bridge will load but +remain inactive. + +## Shared Header + +`rc_ioctl.h` contains all ioctl definitions for both modules. Include +this from userspace C code to use the structured command interfaces. + ## Building ```bash @@ -41,14 +76,23 @@ export CROSS_COMPILE=aarch64-linux-gnu- export KERNEL_DIR=/path/to/kernel/source # Build all modules -make -C $KERNEL_DIR M=$(pwd) modules +make -# Or build individually -make -C $KERNEL_DIR M=$(pwd) CONFIG_RC_WIFI_MON=m modules +# Build a single module +make MOD=rc_wifi_mon + +# Package .ko files for the module zip +make package + +# Clean +make clean ``` ## Runtime Loading -Modules are loaded by the RadioControl service.sh based on detected chipset. -KernelSU module overlay places them in /vendor/lib/modules/ or loads -directly via insmod. +Modules are loaded by RadioControl's service.sh based on detected chipset. +The service automatically: +1. Detects SoC type (Tensor/Exynos/Qualcomm) +2. Loads the appropriate modules (rc_wifi_mon + rc_shannon_cmd for Tensor) +3. Skips rc_diag_bridge on non-Qualcomm devices +4. Verifies module load via /proc/modules diff --git a/common/kmod/rc_diag_bridge.c b/common/kmod/rc_diag_bridge.c index 788f56a..fe79a7a 100644 --- a/common/kmod/rc_diag_bridge.c +++ b/common/kmod/rc_diag_bridge.c @@ -16,6 +16,11 @@ * - FTM commands (subsys 0x4B, subsys_id 11) * - Log mask / message mask configuration * - Raw DIAG passthrough for advanced use + * + * Note: This module is for devices with Qualcomm basebands. + * On Tensor G5 with Shannon 5400, use rc_shannon_cmd instead. + * + * Target: kernel 6.6 */ #include @@ -30,6 +35,8 @@ #include #include #include +#include +#include MODULE_LICENSE("GPL"); MODULE_AUTHOR("RadioControl"); @@ -37,29 +44,58 @@ MODULE_DESCRIPTION("Qualcomm DIAG protocol bridge for NV/EFS/FTM access"); MODULE_VERSION("1.0"); #define DEVICE_NAME "rc_diag" -#define CLASS_NAME "radiocontrol" +#define CLASS_NAME "radiocontrol_diag" #define DIAG_BUF_SIZE 8192 /* DIAG command codes */ #define DIAG_NV_READ_F 0x26 #define DIAG_NV_WRITE_F 0x27 +#define DIAG_STATUS_F 0x0C +#define DIAG_VERNO_F 0x00 #define DIAG_SUBSYS_CMD_F 0x4B #define DIAG_LOG_CONFIG_F 0x73 #define DIAG_MSG_CONFIG_F 0x7D +#define DIAG_EXT_BUILD_ID_F 0x7C /* DIAG subsystem IDs */ #define DIAG_SUBSYS_FTM 11 #define DIAG_SUBSYS_EFS2 19 #define DIAG_SUBSYS_PARAMS 37 +#define DIAG_SUBSYS_DIAG 18 + +/* EFS2 sub-commands (within DIAG_SUBSYS_EFS2) */ +#define EFS2_DIAG_OPEN 0x01 +#define EFS2_DIAG_CLOSE 0x02 +#define EFS2_DIAG_READ 0x03 +#define EFS2_DIAG_WRITE 0x04 +#define EFS2_DIAG_MKDIR 0x06 +#define EFS2_DIAG_OPENDIR 0x08 +#define EFS2_DIAG_READDIR 0x09 +#define EFS2_DIAG_CLOSEDIR 0x0A +#define EFS2_DIAG_STAT 0x0D +#define EFS2_DIAG_UNLINK 0x10 + +/* FTM sub-commands */ +#define FTM_SET_MODE 0x00 +#define FTM_SET_CHAN 0x03 +#define FTM_SET_TX_ON 0x06 +#define FTM_SET_TX_OFF 0x07 +#define FTM_SET_PA_RANGE 0x08 +#define FTM_SET_PDM 0x09 +#define FTM_GET_RSSI 0x26 +#define FTM_GET_RX_LEVEL 0x44 /* IOCTL commands */ #define RC_DIAG_MAGIC 'D' #define RC_DIAG_NV_READ _IOWR(RC_DIAG_MAGIC, 1, struct rc_nv_item) #define RC_DIAG_NV_WRITE _IOW(RC_DIAG_MAGIC, 2, struct rc_nv_item) #define RC_DIAG_RAW_CMD _IOWR(RC_DIAG_MAGIC, 3, struct rc_diag_raw) -#define RC_DIAG_FTM_CMD _IOWR(RC_DIAG_MAGIC, 4, struct rc_diag_raw) +#define RC_DIAG_FTM_CMD _IOWR(RC_DIAG_MAGIC, 4, struct rc_ftm_cmd) #define RC_DIAG_EFS_READ _IOWR(RC_DIAG_MAGIC, 5, struct rc_efs_op) #define RC_DIAG_EFS_WRITE _IOW(RC_DIAG_MAGIC, 6, struct rc_efs_op) +#define RC_DIAG_EFS_STAT _IOWR(RC_DIAG_MAGIC, 7, struct rc_efs_op) +#define RC_DIAG_EFS_UNLINK _IOW(RC_DIAG_MAGIC, 8, struct rc_efs_op) +#define RC_DIAG_GET_VERSION _IOR(RC_DIAG_MAGIC, 9, struct rc_diag_version) /* NV item structure */ struct rc_nv_item { @@ -69,7 +105,7 @@ struct rc_nv_item { uint32_t data_len; }; -/* Raw DIAG command */ +/* Raw DIAG command — allocated on heap due to size */ struct rc_diag_raw { uint8_t cmd[DIAG_BUF_SIZE]; uint32_t cmd_len; @@ -77,12 +113,34 @@ struct rc_diag_raw { uint32_t resp_len; }; +/* FTM command */ +struct rc_ftm_cmd { + uint16_t cmd_id; + uint16_t data_len; + uint8_t data[512]; + uint16_t status; + uint8_t resp[512]; + uint16_t resp_len; +}; + /* EFS operation */ struct rc_efs_op { char path[256]; uint8_t data[4096]; uint32_t data_len; int32_t status; + uint32_t mode; /* file mode for open/mkdir */ + uint32_t offset; /* read/write offset */ +}; + +/* Version info */ +struct rc_diag_version { + char comp_date[12]; + char comp_time[8]; + char rel_date[12]; + char rel_time[8]; + char model[32]; + uint8_t mob_sw_rev; }; /* @@ -93,8 +151,8 @@ struct rc_efs_op { * Escape: 0x7E -> 0x7D 0x5E, 0x7D -> 0x7D 0x5D */ -#define HDLC_FLAG 0x7E -#define HDLC_ESC 0x7D +#define HDLC_FLAG 0x7E +#define HDLC_ESC 0x7D #define HDLC_ESC_MASK 0x20 static const uint16_t crc16_table[256] = { @@ -135,6 +193,7 @@ static const uint16_t crc16_table[256] = { static uint16_t crc16_calc(const uint8_t *buf, int len) { uint16_t crc = 0xFFFF; + while (len--) crc = crc16_table[(crc ^ *buf++) & 0xFF] ^ (crc >> 8); return ~crc & 0xFFFF; @@ -148,6 +207,9 @@ static int hdlc_encode(const uint8_t *src, int src_len, int pos = 0; int i; + if (dst_size < src_len * 2 + 6) + return -ENOMEM; + crc = crc16_calc(src, src_len); dst[pos++] = HDLC_FLAG; @@ -164,6 +226,7 @@ static int hdlc_encode(const uint8_t *src, int src_len, /* Append CRC (little-endian) with escaping */ for (i = 0; i < 2 && pos < dst_size - 2; i++) { uint8_t b = (crc >> (i * 8)) & 0xFF; + if (b == HDLC_FLAG || b == HDLC_ESC) { dst[pos++] = HDLC_ESC; dst[pos++] = b ^ HDLC_ESC_MASK; @@ -176,6 +239,67 @@ static int hdlc_encode(const uint8_t *src, int src_len, return pos; } +/* + * Decode an HDLC-framed DIAG response. + * Strips framing bytes, unescapes, validates CRC. + * Returns payload length (without CRC) or negative error. + */ +static int hdlc_decode(const uint8_t *src, int src_len, + uint8_t *dst, int dst_size) +{ + int pos = 0; + int i; + int start = -1, end = -1; + uint16_t crc_recv, crc_calc; + + /* Find the HDLC frame boundaries */ + for (i = 0; i < src_len; i++) { + if (src[i] == HDLC_FLAG) { + if (start < 0) { + start = i; + } else { + end = i; + break; + } + } + } + + if (start < 0 || end < 0 || end <= start + 1) + return -EINVAL; + + /* Unescape the payload between the flags */ + for (i = start + 1; i < end && pos < dst_size; i++) { + if (src[i] == HDLC_ESC) { + i++; + if (i >= end) + return -EINVAL; + dst[pos++] = src[i] ^ HDLC_ESC_MASK; + } else if (src[i] == HDLC_FLAG) { + break; + } else { + dst[pos++] = src[i]; + } + } + + /* Need at least 2 bytes for CRC + 1 byte payload */ + if (pos < 3) + return -EINVAL; + + /* Last 2 bytes are CRC-16 (little-endian) */ + crc_recv = dst[pos - 2] | (dst[pos - 1] << 8); + pos -= 2; + + /* Validate CRC */ + crc_calc = crc16_calc(dst, pos); + if (crc_calc != crc_recv) { + pr_debug("rc_diag: CRC mismatch: calculated 0x%04x, " + "received 0x%04x\n", crc_calc, crc_recv); + return -EIO; + } + + return pos; +} + static int major; static struct class *rc_class; static struct cdev rc_cdev; @@ -185,50 +309,134 @@ static DEFINE_MUTEX(diag_mutex); static struct file *open_diag_device(void) { + static const char *diag_paths[] = { + "/dev/diag", + "/dev/diag_mdm", + NULL + }; struct file *f; + int i; - f = filp_open("/dev/diag", O_RDWR | O_NONBLOCK, 0); - if (!IS_ERR(f)) - return f; + for (i = 0; diag_paths[i]; i++) { + f = filp_open(diag_paths[i], O_RDWR | O_NONBLOCK, 0); + if (!IS_ERR(f)) { + pr_info("rc_diag: opened %s\n", diag_paths[i]); + return f; + } + } - pr_info("rc_diag: /dev/diag not available (%ld)\n", PTR_ERR(f)); return ERR_PTR(-ENODEV); } +/* + * Send a raw DIAG command (pre-framing) and receive decoded response. + */ static int send_diag_cmd(const uint8_t *cmd, int cmd_len, uint8_t *resp, int resp_size) { - uint8_t hdlc_buf[DIAG_BUF_SIZE * 2]; + uint8_t *hdlc_tx; + uint8_t *hdlc_rx; loff_t pos = 0; ssize_t written, bytes_read; int hdlc_len; - int timeout_ms = 2000; + int timeout_ms = 3000; int elapsed = 0; + int decoded_len; if (!diag_filp || IS_ERR(diag_filp)) return -ENODEV; + hdlc_tx = kmalloc(DIAG_BUF_SIZE * 2, GFP_KERNEL); + hdlc_rx = kmalloc(DIAG_BUF_SIZE * 2, GFP_KERNEL); + if (!hdlc_tx || !hdlc_rx) { + kfree(hdlc_tx); + kfree(hdlc_rx); + return -ENOMEM; + } + /* HDLC encode the command */ - hdlc_len = hdlc_encode(cmd, cmd_len, hdlc_buf, sizeof(hdlc_buf)); + hdlc_len = hdlc_encode(cmd, cmd_len, hdlc_tx, DIAG_BUF_SIZE * 2); + if (hdlc_len < 0) { + kfree(hdlc_tx); + kfree(hdlc_rx); + return hdlc_len; + } /* Send to DIAG */ - written = kernel_write(diag_filp, hdlc_buf, hdlc_len, &pos); - if (written < 0) - return written; + written = kernel_write(diag_filp, hdlc_tx, hdlc_len, &pos); + kfree(hdlc_tx); - /* Read response */ + if (written < 0) { + kfree(hdlc_rx); + return written; + } + + /* Read response with timeout */ pos = 0; while (elapsed < timeout_ms) { - bytes_read = kernel_read(diag_filp, resp, resp_size, &pos); - if (bytes_read > 0) - return bytes_read; + bytes_read = kernel_read(diag_filp, hdlc_rx, + DIAG_BUF_SIZE * 2, &pos); + if (bytes_read > 0) { + /* Decode HDLC frame */ + decoded_len = hdlc_decode(hdlc_rx, bytes_read, + resp, resp_size); + kfree(hdlc_rx); + + if (decoded_len < 0) { + pr_debug("rc_diag: HDLC decode failed: %d, " + "returning raw (%zd bytes)\n", + decoded_len, bytes_read); + /* + * Some DIAG drivers return pre-decoded data. + * Fall back to raw copy. + */ + if (bytes_read <= resp_size) { + memcpy(resp, hdlc_rx, bytes_read); + return bytes_read; + } + return decoded_len; + } + return decoded_len; + } msleep(20); elapsed += 20; } + kfree(hdlc_rx); return -ETIMEDOUT; } +/* + * Build and send a subsystem command. + * Format: [0x4B] [subsys_id] [sub_cmd LE16] [payload...] + */ +static int send_subsys_cmd(uint8_t subsys_id, uint16_t sub_cmd, + const uint8_t *payload, int payload_len, + uint8_t *resp, int resp_size) +{ + uint8_t *cmd; + int cmd_len = 4 + payload_len; + int ret; + + cmd = kmalloc(cmd_len, GFP_KERNEL); + if (!cmd) + return -ENOMEM; + + cmd[0] = DIAG_SUBSYS_CMD_F; + cmd[1] = subsys_id; + cmd[2] = sub_cmd & 0xFF; + cmd[3] = (sub_cmd >> 8) & 0xFF; + if (payload_len > 0) + memcpy(cmd + 4, payload, payload_len); + + ret = send_diag_cmd(cmd, cmd_len, resp, resp_size); + kfree(cmd); + return ret; +} + +/* + * IOCTL handlers + */ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { int ret = 0; @@ -253,11 +461,21 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) ret = send_diag_cmd(diag_cmd, 3, diag_resp, sizeof(diag_resp)); if (ret > 0) { - /* Response: [0x26] [NV_ID] [status] [data...] */ - nv.status = diag_resp[3] | (diag_resp[4] << 8); - nv.data_len = ret > 133 ? 128 : ret - 5; - if (nv.data_len > 0) - memcpy(nv.data, diag_resp + 5, nv.data_len); + /* + * Response format: + * [0x26] [NV_ID LE 16] [status LE 16] [data...] + * Status: 0=OK, 5=inactive, 6=bad_security + */ + if (ret < 5) { + nv.status = 0xFFFF; + nv.data_len = 0; + } else { + nv.status = diag_resp[3] | (diag_resp[4] << 8); + nv.data_len = (ret > 133) ? 128 : ret - 5; + if (nv.data_len > 0) + memcpy(nv.data, diag_resp + 5, + nv.data_len); + } if (copy_to_user((void __user *)arg, &nv, sizeof(nv))) ret = -EFAULT; else @@ -290,7 +508,10 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) ret = send_diag_cmd(diag_cmd, 3 + nv.data_len, diag_resp, sizeof(diag_resp)); if (ret > 0) { - nv.status = diag_resp[3] | (diag_resp[4] << 8); + if (ret >= 5) + nv.status = diag_resp[3] | (diag_resp[4] << 8); + else + nv.status = 0xFFFF; if (copy_to_user((void __user *)arg, &nv, sizeof(nv))) ret = -EFAULT; else @@ -301,8 +522,12 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) case RC_DIAG_RAW_CMD: { struct rc_diag_raw *raw; + raw = kmalloc(sizeof(*raw), GFP_KERNEL); - if (!raw) { ret = -ENOMEM; break; } + if (!raw) { + ret = -ENOMEM; + break; + } if (copy_from_user(raw, (void __user *)arg, sizeof(*raw))) { kfree(raw); @@ -310,11 +535,18 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) break; } + if (raw->cmd_len == 0 || raw->cmd_len > DIAG_BUF_SIZE) { + kfree(raw); + ret = -EINVAL; + break; + } + ret = send_diag_cmd(raw->cmd, raw->cmd_len, raw->resp, sizeof(raw->resp)); if (ret > 0) { raw->resp_len = ret; - if (copy_to_user((void __user *)arg, raw, sizeof(*raw))) + if (copy_to_user((void __user *)arg, raw, + sizeof(*raw))) ret = -EFAULT; else ret = 0; @@ -323,6 +555,393 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) break; } + case RC_DIAG_FTM_CMD: { + struct rc_ftm_cmd ftm; + uint8_t payload[520]; + uint8_t resp[256]; + int payload_len; + + if (copy_from_user(&ftm, (void __user *)arg, sizeof(ftm))) { + ret = -EFAULT; + break; + } + + if (ftm.data_len > sizeof(ftm.data)) { + ret = -EINVAL; + break; + } + + /* + * FTM payload format (within subsys command): + * [cmd_id LE16] [data_len LE16] [data...] + */ + payload[0] = ftm.cmd_id & 0xFF; + payload[1] = (ftm.cmd_id >> 8) & 0xFF; + payload[2] = ftm.data_len & 0xFF; + payload[3] = (ftm.data_len >> 8) & 0xFF; + if (ftm.data_len > 0) + memcpy(payload + 4, ftm.data, ftm.data_len); + payload_len = 4 + ftm.data_len; + + ret = send_subsys_cmd(DIAG_SUBSYS_FTM, ftm.cmd_id, + payload, payload_len, + resp, sizeof(resp)); + if (ret > 0) { + /* + * FTM response: + * [0x4B] [subsys_id] [cmd LE16] [status LE16] [data...] + */ + if (ret >= 6) + ftm.status = resp[4] | (resp[5] << 8); + else + ftm.status = 0xFFFF; + + ftm.resp_len = (ret > 6) ? ret - 6 : 0; + if (ftm.resp_len > sizeof(ftm.resp)) + ftm.resp_len = sizeof(ftm.resp); + if (ftm.resp_len > 0) + memcpy(ftm.resp, resp + 6, ftm.resp_len); + + if (copy_to_user((void __user *)arg, &ftm, + sizeof(ftm))) + ret = -EFAULT; + else + ret = 0; + } + break; + } + + case RC_DIAG_EFS_READ: { + struct rc_efs_op *efs; + uint8_t open_payload[264]; + uint8_t read_payload[12]; + uint8_t resp[4224]; + int path_len; + int32_t fd; + + efs = kmalloc(sizeof(*efs), GFP_KERNEL); + if (!efs) { + ret = -ENOMEM; + break; + } + + if (copy_from_user(efs, (void __user *)arg, sizeof(*efs))) { + kfree(efs); + ret = -EFAULT; + break; + } + + efs->path[sizeof(efs->path) - 1] = '\0'; + path_len = strlen(efs->path); + + /* + * Step 1: EFS2_DIAG_OPEN + * Payload: [oflag LE32] [mode LE32] [path (null-terminated)] + */ + open_payload[0] = 0x00; /* O_RDONLY */ + open_payload[1] = 0x00; + open_payload[2] = 0x00; + open_payload[3] = 0x00; + open_payload[4] = 0x00; /* mode (ignored for read) */ + open_payload[5] = 0x00; + open_payload[6] = 0x00; + open_payload[7] = 0x00; + memcpy(open_payload + 8, efs->path, path_len + 1); + + ret = send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_OPEN, + open_payload, 8 + path_len + 1, + resp, sizeof(resp)); + if (ret < 8) { + efs->status = -EIO; + goto efs_read_out; + } + + /* Response: [hdr 4B] [fd LE32] [errno LE32] */ + fd = resp[4] | (resp[5] << 8) | + (resp[6] << 16) | (resp[7] << 24); + + if (fd < 0) { + efs->status = fd; + goto efs_read_out; + } + + /* + * Step 2: EFS2_DIAG_READ + * Payload: [fd LE32] [nbytes LE32] [offset LE32] + */ + read_payload[0] = fd & 0xFF; + read_payload[1] = (fd >> 8) & 0xFF; + read_payload[2] = (fd >> 16) & 0xFF; + read_payload[3] = (fd >> 24) & 0xFF; + read_payload[4] = 0x00; /* nbytes = 4096 */ + read_payload[5] = 0x10; + read_payload[6] = 0x00; + read_payload[7] = 0x00; + read_payload[8] = efs->offset & 0xFF; + read_payload[9] = (efs->offset >> 8) & 0xFF; + read_payload[10] = (efs->offset >> 16) & 0xFF; + read_payload[11] = (efs->offset >> 24) & 0xFF; + + ret = send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_READ, + read_payload, 12, resp, sizeof(resp)); + if (ret > 12) { + /* + * Response: [hdr 4B] [fd LE32] [offset LE32] + * [bytes_read LE32] [errno LE32] [data...] + */ + int32_t bytes_read = resp[12] | (resp[13] << 8) | + (resp[14] << 16) | (resp[15] << 24); + int32_t efs_errno = resp[16] | (resp[17] << 8) | + (resp[18] << 16) | (resp[19] << 24); + + if (bytes_read > 0 && efs_errno == 0) { + efs->data_len = min_t(uint32_t, bytes_read, + sizeof(efs->data)); + memcpy(efs->data, resp + 20, efs->data_len); + efs->status = 0; + } else { + efs->data_len = 0; + efs->status = efs_errno ? -efs_errno : -EIO; + } + } else { + efs->status = -EIO; + } + + /* + * Step 3: EFS2_DIAG_CLOSE + * Payload: [fd LE32] + */ + send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_CLOSE, + read_payload, 4, resp, sizeof(resp)); + +efs_read_out: + if (copy_to_user((void __user *)arg, efs, sizeof(*efs))) + ret = -EFAULT; + else + ret = 0; + kfree(efs); + break; + } + + case RC_DIAG_EFS_WRITE: { + struct rc_efs_op *efs; + uint8_t open_payload[264]; + uint8_t write_payload[4108]; + uint8_t resp[64]; + int path_len; + int32_t fd; + + efs = kmalloc(sizeof(*efs), GFP_KERNEL); + if (!efs) { + ret = -ENOMEM; + break; + } + + if (copy_from_user(efs, (void __user *)arg, sizeof(*efs))) { + kfree(efs); + ret = -EFAULT; + break; + } + + efs->path[sizeof(efs->path) - 1] = '\0'; + path_len = strlen(efs->path); + + if (efs->data_len > sizeof(efs->data)) { + kfree(efs); + ret = -EINVAL; + break; + } + + /* + * Step 1: EFS2_DIAG_OPEN (O_WRONLY | O_CREAT | O_TRUNC) + * oflag = 0x0601 + */ + open_payload[0] = 0x01; + open_payload[1] = 0x06; + open_payload[2] = 0x00; + open_payload[3] = 0x00; + /* mode: use provided or default 0644 */ + open_payload[4] = (efs->mode ? efs->mode : 0644) & 0xFF; + open_payload[5] = ((efs->mode ? efs->mode : 0644) >> 8) & 0xFF; + open_payload[6] = 0x00; + open_payload[7] = 0x00; + memcpy(open_payload + 8, efs->path, path_len + 1); + + ret = send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_OPEN, + open_payload, 8 + path_len + 1, + resp, sizeof(resp)); + if (ret < 8) { + efs->status = -EIO; + goto efs_write_out; + } + + fd = resp[4] | (resp[5] << 8) | + (resp[6] << 16) | (resp[7] << 24); + if (fd < 0) { + efs->status = fd; + goto efs_write_out; + } + + /* + * Step 2: EFS2_DIAG_WRITE + * Payload: [fd LE32] [offset LE32] [data...] + */ + write_payload[0] = fd & 0xFF; + write_payload[1] = (fd >> 8) & 0xFF; + write_payload[2] = (fd >> 16) & 0xFF; + write_payload[3] = (fd >> 24) & 0xFF; + write_payload[4] = efs->offset & 0xFF; + write_payload[5] = (efs->offset >> 8) & 0xFF; + write_payload[6] = (efs->offset >> 16) & 0xFF; + write_payload[7] = (efs->offset >> 24) & 0xFF; + memcpy(write_payload + 8, efs->data, efs->data_len); + + ret = send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_WRITE, + write_payload, 8 + efs->data_len, + resp, sizeof(resp)); + if (ret > 8) { + int32_t efs_errno = resp[8] | (resp[9] << 8) | + (resp[10] << 16) | (resp[11] << 24); + efs->status = efs_errno ? -efs_errno : 0; + } else { + efs->status = -EIO; + } + + /* Step 3: Close */ + send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_CLOSE, + write_payload, 4, resp, sizeof(resp)); + +efs_write_out: + if (copy_to_user((void __user *)arg, efs, sizeof(*efs))) + ret = -EFAULT; + else + ret = 0; + kfree(efs); + break; + } + + case RC_DIAG_EFS_STAT: { + struct rc_efs_op *efs; + uint8_t payload[264]; + uint8_t resp[64]; + int path_len; + + efs = kmalloc(sizeof(*efs), GFP_KERNEL); + if (!efs) { + ret = -ENOMEM; + break; + } + + if (copy_from_user(efs, (void __user *)arg, sizeof(*efs))) { + kfree(efs); + ret = -EFAULT; + break; + } + + efs->path[sizeof(efs->path) - 1] = '\0'; + path_len = strlen(efs->path); + memcpy(payload, efs->path, path_len + 1); + + ret = send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_STAT, + payload, path_len + 1, + resp, sizeof(resp)); + if (ret > 4) { + int32_t efs_errno = resp[4] | (resp[5] << 8) | + (resp[6] << 16) | (resp[7] << 24); + efs->status = efs_errno ? -efs_errno : 0; + + if (efs->status == 0 && ret >= 16) { + /* mode at offset 8, size at offset 12 */ + efs->mode = resp[8] | (resp[9] << 8) | + (resp[10] << 16) | (resp[11] << 24); + efs->data_len = resp[12] | (resp[13] << 8) | + (resp[14] << 16) | (resp[15] << 24); + } + } else { + efs->status = -EIO; + } + + if (copy_to_user((void __user *)arg, efs, sizeof(*efs))) + ret = -EFAULT; + else + ret = 0; + kfree(efs); + break; + } + + case RC_DIAG_EFS_UNLINK: { + struct rc_efs_op *efs; + uint8_t payload[264]; + uint8_t resp[32]; + int path_len; + + efs = kmalloc(sizeof(*efs), GFP_KERNEL); + if (!efs) { + ret = -ENOMEM; + break; + } + + if (copy_from_user(efs, (void __user *)arg, sizeof(*efs))) { + kfree(efs); + ret = -EFAULT; + break; + } + + efs->path[sizeof(efs->path) - 1] = '\0'; + path_len = strlen(efs->path); + memcpy(payload, efs->path, path_len + 1); + + ret = send_subsys_cmd(DIAG_SUBSYS_EFS2, EFS2_DIAG_UNLINK, + payload, path_len + 1, + resp, sizeof(resp)); + if (ret > 4) { + int32_t efs_errno = resp[4] | (resp[5] << 8) | + (resp[6] << 16) | (resp[7] << 24); + efs->status = efs_errno ? -efs_errno : 0; + } else { + efs->status = -EIO; + } + + if (copy_to_user((void __user *)arg, efs, sizeof(*efs))) + ret = -EFAULT; + else + ret = 0; + kfree(efs); + break; + } + + case RC_DIAG_GET_VERSION: { + struct rc_diag_version ver; + uint8_t diag_cmd = DIAG_VERNO_F; + uint8_t resp[128]; + + memset(&ver, 0, sizeof(ver)); + + ret = send_diag_cmd(&diag_cmd, 1, resp, sizeof(resp)); + if (ret > 0) { + /* + * Version response: + * [0x00] [comp_date 11B] [comp_time 8B] + * [rel_date 11B] [rel_time 8B] [model...] + */ + if (ret >= 40) { + memcpy(ver.comp_date, resp + 1, 11); + memcpy(ver.comp_time, resp + 12, 8); + memcpy(ver.rel_date, resp + 20, 11); + memcpy(ver.rel_time, resp + 31, 8); + } + if (ret >= 42) + ver.mob_sw_rev = resp[39]; + + if (copy_to_user((void __user *)arg, &ver, + sizeof(ver))) + ret = -EFAULT; + else + ret = 0; + } + break; + } + default: ret = -ENOTTY; } @@ -334,7 +953,7 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) static const struct file_operations rc_fops = { .owner = THIS_MODULE, .unlocked_ioctl = rc_ioctl, - .compat_ioctl = rc_ioctl, + .compat_ioctl = compat_ptr_ioctl, }; static int __init rc_diag_init(void) @@ -356,30 +975,43 @@ static int __init rc_diag_init(void) return ret; major = MAJOR(dev); - rc_class = class_create(THIS_MODULE, CLASS_NAME "_diag"); +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0) + rc_class = class_create(CLASS_NAME); +#else + rc_class = class_create(THIS_MODULE, CLASS_NAME); +#endif if (IS_ERR(rc_class)) { ret = PTR_ERR(rc_class); goto err; } cdev_init(&rc_cdev, &rc_fops); - cdev_add(&rc_cdev, MKDEV(major, 0), 1); + rc_cdev.owner = THIS_MODULE; + + ret = cdev_add(&rc_cdev, MKDEV(major, 0), 1); + if (ret < 0) + goto err_cdev; rc_device = device_create(rc_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME); if (IS_ERR(rc_device)) { ret = PTR_ERR(rc_device); - goto err2; + goto err_device; } - pr_info("rc_diag: /dev/%s created\n", DEVICE_NAME); + pr_info("rc_diag: /dev/%s created (major %d)%s\n", + DEVICE_NAME, major, + diag_filp ? "" : " [inactive — no Qualcomm modem]"); return 0; -err2: +err_device: cdev_del(&rc_cdev); +err_cdev: class_destroy(rc_class); err: unregister_chrdev_region(MKDEV(major, 0), 1); + if (diag_filp) + filp_close(diag_filp, NULL); return ret; } diff --git a/common/kmod/rc_ioctl.h b/common/kmod/rc_ioctl.h new file mode 100644 index 0000000..dc0ca84 --- /dev/null +++ b/common/kmod/rc_ioctl.h @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * rc_ioctl.h — Shared ioctl definitions for RadioControl kernel modules + * + * Include from userspace to use the ioctl interfaces on: + * /dev/rc_shannon (Shannon modem AT commands) + * /dev/rc_diag (Qualcomm DIAG protocol) + */ + +#ifndef _RC_IOCTL_H +#define _RC_IOCTL_H + +#include +#include + +/* ---- /dev/rc_shannon ioctls ---- */ + +#define RC_SHANNON_MAGIC 'S' + +struct rc_at_cmd { + char cmd[4096]; + __u32 cmd_len; + char resp[8192]; + __u32 resp_len; + __u32 timeout_ms; /* 0 = use default (5000ms) */ + __s32 status; /* 0=OK, -1=ERROR, -2=TIMEOUT, -3=CME ERROR */ +}; + +struct rc_urc_msg { + char data[1024]; + __u32 data_len; + __s32 remaining; /* URCs still queued */ +}; + +struct rc_modem_status { + char device_path[128]; + __s32 connected; + __s32 urc_count; + __u64 cmds_sent; + __u64 cmds_failed; + __u64 bytes_tx; + __u64 bytes_rx; +}; + +#define RC_SHANNON_AT_CMD _IOWR(RC_SHANNON_MAGIC, 1, struct rc_at_cmd) +#define RC_SHANNON_GET_URC _IOR(RC_SHANNON_MAGIC, 2, struct rc_urc_msg) +#define RC_SHANNON_SET_TIMEOUT _IOW(RC_SHANNON_MAGIC, 3, int) +#define RC_SHANNON_GET_STATUS _IOR(RC_SHANNON_MAGIC, 4, struct rc_modem_status) +#define RC_SHANNON_FLUSH _IO(RC_SHANNON_MAGIC, 5) + +/* ---- /dev/rc_diag ioctls ---- */ + +#define RC_DIAG_MAGIC 'D' + +struct rc_nv_item { + __u16 id; + __u16 status; /* 0 = success */ + __u8 data[128]; + __u32 data_len; +}; + +struct rc_diag_raw { + __u8 cmd[8192]; + __u32 cmd_len; + __u8 resp[8192]; + __u32 resp_len; +}; + +struct rc_ftm_cmd { + __u16 cmd_id; + __u16 data_len; + __u8 data[512]; + __u16 status; + __u8 resp[512]; + __u16 resp_len; +}; + +struct rc_efs_op { + char path[256]; + __u8 data[4096]; + __u32 data_len; + __s32 status; + __u32 mode; /* file mode for open/mkdir */ + __u32 offset; /* read/write offset */ +}; + +struct rc_diag_version { + char comp_date[12]; + char comp_time[8]; + char rel_date[12]; + char rel_time[8]; + char model[32]; + __u8 mob_sw_rev; +}; + +#define RC_DIAG_NV_READ _IOWR(RC_DIAG_MAGIC, 1, struct rc_nv_item) +#define RC_DIAG_NV_WRITE _IOW(RC_DIAG_MAGIC, 2, struct rc_nv_item) +#define RC_DIAG_RAW_CMD _IOWR(RC_DIAG_MAGIC, 3, struct rc_diag_raw) +#define RC_DIAG_FTM_CMD _IOWR(RC_DIAG_MAGIC, 4, struct rc_ftm_cmd) +#define RC_DIAG_EFS_READ _IOWR(RC_DIAG_MAGIC, 5, struct rc_efs_op) +#define RC_DIAG_EFS_WRITE _IOW(RC_DIAG_MAGIC, 6, struct rc_efs_op) +#define RC_DIAG_EFS_STAT _IOWR(RC_DIAG_MAGIC, 7, struct rc_efs_op) +#define RC_DIAG_EFS_UNLINK _IOW(RC_DIAG_MAGIC, 8, struct rc_efs_op) +#define RC_DIAG_GET_VERSION _IOR(RC_DIAG_MAGIC, 9, struct rc_diag_version) + +#endif /* _RC_IOCTL_H */ diff --git a/common/kmod/rc_shannon_cmd.c b/common/kmod/rc_shannon_cmd.c index c067ca1..9a8c45a 100644 --- a/common/kmod/rc_shannon_cmd.c +++ b/common/kmod/rc_shannon_cmd.c @@ -6,20 +6,23 @@ * read responses from Samsung Shannon modem, bypassing the RIL * lock on /dev/umts_router0. * - * On Exynos: talks to Shannon via /dev/umts_atc0 or umts_router0 - * On Tensor: same Shannon modem, paths may be /dev/nr_atc0 + * On Tensor G5: Shannon 5400 (S5400BUNUELO), paths include + * /dev/umts_router, /dev/umts_atc0, /dev/nr_atc0 * * This module: * 1. Opens the underlying modem char device from kernel space * 2. Creates /dev/rc_shannon as a proxy with proper queuing * 3. Multiplexes between RadioControl userspace and the modem - * 4. Prevents RIL from monopolizing the AT channel + * 4. Handles URCs (unsolicited result codes) from the modem + * 5. Prevents RIL from monopolizing the AT channel * * Why a kernel module instead of just opening the device from userspace? * - The RIL daemon holds /dev/umts_router0 open exclusively * - Even with root, opening it races with RIL and can crash the modem * - This module uses a secondary AT channel (atc0/atc1) that RIL * doesn't claim, or creates a proper multiplexed path + * + * Target: Pixel 10 Pro Fold (rango), Tensor G5, kernel 6.6 */ #include @@ -35,6 +38,9 @@ #include #include #include +#include +#include +#include MODULE_LICENSE("GPL"); MODULE_AUTHOR("RadioControl"); @@ -43,14 +49,54 @@ MODULE_VERSION("1.0"); #define DEVICE_NAME "rc_shannon" #define CLASS_NAME "radiocontrol" -#define BUF_SIZE 4096 +#define CMD_BUF_SIZE 4096 +#define RESP_BUF_SIZE 8192 +#define URC_BUF_SIZE 16384 +#define MAX_CLIENTS 4 + +/* IOCTL commands */ +#define RC_SHANNON_MAGIC 'S' +#define RC_SHANNON_AT_CMD _IOWR(RC_SHANNON_MAGIC, 1, struct rc_at_cmd) +#define RC_SHANNON_GET_URC _IOR(RC_SHANNON_MAGIC, 2, struct rc_urc_msg) +#define RC_SHANNON_SET_TIMEOUT _IOW(RC_SHANNON_MAGIC, 3, int) +#define RC_SHANNON_GET_STATUS _IOR(RC_SHANNON_MAGIC, 4, struct rc_modem_status) +#define RC_SHANNON_FLUSH _IO(RC_SHANNON_MAGIC, 5) + +/* AT command with explicit timeout */ +struct rc_at_cmd { + char cmd[CMD_BUF_SIZE]; + uint32_t cmd_len; + char resp[RESP_BUF_SIZE]; + uint32_t resp_len; + uint32_t timeout_ms; /* 0 = use default */ + int32_t status; /* 0=OK, -1=ERROR, -2=TIMEOUT, -3=CME ERROR */ +}; + +/* Unsolicited result code */ +struct rc_urc_msg { + char data[1024]; + uint32_t data_len; + int32_t remaining; /* URCs still queued */ +}; + +/* Modem status info */ +struct rc_modem_status { + char device_path[128]; + int32_t connected; + int32_t urc_count; + uint64_t cmds_sent; + uint64_t cmds_failed; + uint64_t bytes_tx; + uint64_t bytes_rx; +}; /* Modem device paths to try, in order of preference */ static const char *modem_paths[] = { "/dev/umts_atc0", /* Secondary AT channel — not claimed by RIL */ "/dev/umts_atc1", /* Tertiary AT channel */ "/dev/nr_atc0", /* Tensor NR naming */ - "/dev/umts_router0", /* Primary — last resort, RIL conflict risk */ + "/dev/umts_router", /* Tensor primary (no trailing 0) */ + "/dev/umts_router0", /* Exynos primary — last resort */ NULL }; @@ -59,14 +105,124 @@ static struct class *rc_class; static struct cdev rc_cdev; static struct device *rc_device; static struct file *modem_filp; +static char modem_path_used[128]; static DEFINE_MUTEX(cmd_mutex); static DECLARE_WAIT_QUEUE_HEAD(resp_waitq); +static DECLARE_WAIT_QUEUE_HEAD(urc_waitq); -/* Response buffer */ -static char resp_buf[BUF_SIZE]; +/* Response buffer for synchronous command/response */ +static char resp_buf[RESP_BUF_SIZE]; static int resp_len; static bool resp_ready; +/* URC circular buffer */ +struct urc_entry { + char data[1024]; + int len; +}; +static struct urc_entry urc_ring[64]; +static int urc_head; +static int urc_tail; +static DEFINE_SPINLOCK(urc_lock); + +/* Statistics */ +static uint64_t stat_cmds_sent; +static uint64_t stat_cmds_failed; +static uint64_t stat_bytes_tx; +static uint64_t stat_bytes_rx; + +/* Reader thread */ +static struct task_struct *reader_thread; +static int default_timeout_ms = 5000; + +/* Common URC prefixes from Shannon modems */ +static const char *urc_prefixes[] = { + "+CRING:", "+CLIP:", "+CREG:", "+CGREG:", + "+CEREG:", "+C5GREG:", "+CMTI:", "+CMT:", + "+CDS:", "+CUSD:", "+CCWA:", "+CSSI:", + "+CSSU:", "+COPS:", "RING", "NO CARRIER", + "+CGEV:", "+CIEV:", "+AIMS", "$", + NULL +}; + +static bool is_urc(const char *line) +{ + int i; + + /* Skip leading \r\n */ + while (*line == '\r' || *line == '\n') + line++; + + for (i = 0; urc_prefixes[i]; i++) { + if (strncmp(line, urc_prefixes[i], + strlen(urc_prefixes[i])) == 0) + return true; + } + return false; +} + +static void urc_enqueue(const char *data, int len) +{ + unsigned long flags; + int next; + + spin_lock_irqsave(&urc_lock, flags); + next = (urc_head + 1) % ARRAY_SIZE(urc_ring); + if (next == urc_tail) { + /* Ring full — drop oldest */ + urc_tail = (urc_tail + 1) % ARRAY_SIZE(urc_ring); + } + if (len > sizeof(urc_ring[0].data) - 1) + len = sizeof(urc_ring[0].data) - 1; + memcpy(urc_ring[urc_head].data, data, len); + urc_ring[urc_head].data[len] = '\0'; + urc_ring[urc_head].len = len; + urc_head = next; + spin_unlock_irqrestore(&urc_lock, flags); + + wake_up_interruptible(&urc_waitq); +} + +static int urc_dequeue(struct rc_urc_msg *msg) +{ + unsigned long flags; + int count; + + spin_lock_irqsave(&urc_lock, flags); + if (urc_head == urc_tail) { + spin_unlock_irqrestore(&urc_lock, flags); + return -EAGAIN; + } + msg->data_len = urc_ring[urc_tail].len; + memcpy(msg->data, urc_ring[urc_tail].data, msg->data_len); + msg->data[msg->data_len] = '\0'; + urc_tail = (urc_tail + 1) % ARRAY_SIZE(urc_ring); + + /* Count remaining */ + if (urc_head >= urc_tail) + count = urc_head - urc_tail; + else + count = ARRAY_SIZE(urc_ring) - urc_tail + urc_head; + msg->remaining = count; + spin_unlock_irqrestore(&urc_lock, flags); + + return 0; +} + +static int urc_count(void) +{ + unsigned long flags; + int count; + + spin_lock_irqsave(&urc_lock, flags); + if (urc_head >= urc_tail) + count = urc_head - urc_tail; + else + count = ARRAY_SIZE(urc_ring) - urc_tail + urc_head; + spin_unlock_irqrestore(&urc_lock, flags); + return count; +} + /* * Open the underlying modem device from kernel context. */ @@ -78,6 +234,8 @@ static struct file *open_modem_device(void) for (i = 0; modem_paths[i]; i++) { f = filp_open(modem_paths[i], O_RDWR | O_NONBLOCK, 0); if (!IS_ERR(f)) { + strscpy(modem_path_used, modem_paths[i], + sizeof(modem_path_used)); pr_info("rc_shannon: opened modem device: %s\n", modem_paths[i]); return f; @@ -91,46 +249,104 @@ static struct file *open_modem_device(void) /* * Send an AT command to the modem and read the response. + * Separates URCs from command responses. */ static int send_at_command(const char *cmd, int cmd_len, - char *response, int resp_size) + char *response, int resp_size, int timeout_ms) { loff_t pos = 0; ssize_t written, bytes_read; - int timeout_ms = 3000; int elapsed = 0; int total_read = 0; + char line_buf[1024]; + int line_pos = 0; + bool in_response = false; if (!modem_filp || IS_ERR(modem_filp)) return -ENODEV; + if (timeout_ms <= 0) + timeout_ms = default_timeout_ms; + /* Write command to modem */ written = kernel_write(modem_filp, cmd, cmd_len, &pos); if (written < 0) { pr_err("rc_shannon: write failed: %zd\n", written); + stat_cmds_failed++; return written; } + stat_bytes_tx += written; + stat_cmds_sent++; - /* Read response with timeout */ + /* Read response with timeout, filtering URCs */ memset(response, 0, resp_size); pos = 0; while (elapsed < timeout_ms && total_read < resp_size - 1) { - bytes_read = kernel_read(modem_filp, response + total_read, - resp_size - 1 - total_read, &pos); + char tmp[512]; + + bytes_read = kernel_read(modem_filp, tmp, sizeof(tmp), &pos); if (bytes_read > 0) { - total_read += bytes_read; - /* Check for final response */ - if (strnstr(response, "\r\nOK\r\n", total_read) || - strnstr(response, "\r\nERROR\r\n", total_read) || - strnstr(response, "\r\n+CME ERROR:", total_read)) - break; + int i; + + stat_bytes_rx += bytes_read; + + for (i = 0; i < bytes_read; i++) { + char c = tmp[i]; + + /* Build lines to check for URCs */ + if (c == '\n' || line_pos >= sizeof(line_buf) - 1) { + line_buf[line_pos] = '\0'; + + if (line_pos > 0 && is_urc(line_buf)) { + /* It's a URC — queue it, don't add to response */ + urc_enqueue(line_buf, line_pos); + } else { + /* Part of command response */ + if (total_read + line_pos + 1 < resp_size) { + memcpy(response + total_read, + line_buf, line_pos); + total_read += line_pos; + response[total_read++] = '\n'; + in_response = true; + } + } + line_pos = 0; + } else if (c != '\r') { + line_buf[line_pos++] = c; + } + } + + /* Check for final response in accumulated data */ + if (in_response) { + if (strnstr(response, "OK", total_read) || + strnstr(response, "ERROR", total_read) || + strnstr(response, "+CME ERROR:", total_read) || + strnstr(response, "+CMS ERROR:", total_read)) + break; + } } else { - msleep(50); - elapsed += 50; + msleep(20); + elapsed += 20; } } + /* Flush any remaining partial line */ + if (line_pos > 0) { + line_buf[line_pos] = '\0'; + if (is_urc(line_buf)) { + urc_enqueue(line_buf, line_pos); + } else if (total_read + line_pos < resp_size) { + memcpy(response + total_read, line_buf, line_pos); + total_read += line_pos; + } + } + + response[total_read] = '\0'; + + if (elapsed >= timeout_ms && total_read == 0) + return -ETIMEDOUT; + return total_read; } @@ -147,13 +363,16 @@ static int rc_release(struct inode *inode, struct file *file) return 0; } +/* + * write() — send AT command, response available via read() + */ static ssize_t rc_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { - char cmd_buf[BUF_SIZE]; + char cmd_buf[CMD_BUF_SIZE]; int ret; - if (count >= BUF_SIZE) + if (count >= CMD_BUF_SIZE - 2) return -EINVAL; if (copy_from_user(cmd_buf, buf, count)) @@ -162,12 +381,17 @@ static ssize_t rc_write(struct file *file, const char __user *buf, mutex_lock(&cmd_mutex); - /* Ensure command ends with \r\n */ + /* Ensure command ends with \r\n for the Shannon modem */ if (count >= 2 && cmd_buf[count-2] == '\r' && cmd_buf[count-1] == '\n') { /* Already terminated */ } else if (count >= 1 && cmd_buf[count-1] == '\r') { cmd_buf[count] = '\n'; count++; + } else if (count >= 1 && cmd_buf[count-1] == '\n') { + /* Shift to insert \r before \n */ + cmd_buf[count] = cmd_buf[count-1]; + cmd_buf[count-1] = '\r'; + count++; } else { cmd_buf[count] = '\r'; cmd_buf[count+1] = '\n'; @@ -175,18 +399,25 @@ static ssize_t rc_write(struct file *file, const char __user *buf, } cmd_buf[count] = '\0'; - ret = send_at_command(cmd_buf, count, resp_buf, BUF_SIZE); + ret = send_at_command(cmd_buf, count, resp_buf, RESP_BUF_SIZE, + default_timeout_ms); if (ret >= 0) { resp_len = ret; resp_ready = true; wake_up_interruptible(&resp_waitq); + } else { + resp_len = 0; + resp_ready = false; } mutex_unlock(&cmd_mutex); - return ret >= 0 ? count : ret; + return ret >= 0 ? (ssize_t)count : ret; } +/* + * read() — get the response from the last AT command + */ static ssize_t rc_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { @@ -195,7 +426,8 @@ static ssize_t rc_read(struct file *file, char __user *buf, if (!resp_ready) { if (file->f_flags & O_NONBLOCK) return -EAGAIN; - wait_event_interruptible(resp_waitq, resp_ready); + if (wait_event_interruptible(resp_waitq, resp_ready)) + return -ERESTARTSYS; } if (!resp_ready) @@ -209,24 +441,178 @@ static ssize_t rc_read(struct file *file, char __user *buf, return to_copy; } -static unsigned int rc_poll(struct file *file, poll_table *wait) +static __poll_t rc_poll(struct file *file, poll_table *wait) { - unsigned int mask = POLLOUT | POLLWRNORM; + __poll_t mask = EPOLLOUT | EPOLLWRNORM; poll_wait(file, &resp_waitq, wait); + poll_wait(file, &urc_waitq, wait); + if (resp_ready) - mask |= POLLIN | POLLRDNORM; + mask |= EPOLLIN | EPOLLRDNORM; + if (urc_count() > 0) + mask |= EPOLLPRI; /* URCs available via ioctl */ return mask; } +/* + * ioctl — structured AT command interface + */ +static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + int ret = 0; + + switch (cmd) { + case RC_SHANNON_AT_CMD: { + struct rc_at_cmd *at; + + at = kmalloc(sizeof(*at), GFP_KERNEL); + if (!at) + return -ENOMEM; + + if (copy_from_user(at, (void __user *)arg, sizeof(*at))) { + kfree(at); + return -EFAULT; + } + + /* Sanity checks */ + if (at->cmd_len == 0 || at->cmd_len >= CMD_BUF_SIZE) { + kfree(at); + return -EINVAL; + } + at->cmd[at->cmd_len] = '\0'; + + /* Ensure \r\n termination */ + if (at->cmd_len < 2 || + at->cmd[at->cmd_len-2] != '\r' || + at->cmd[at->cmd_len-1] != '\n') { + at->cmd[at->cmd_len++] = '\r'; + at->cmd[at->cmd_len++] = '\n'; + at->cmd[at->cmd_len] = '\0'; + } + + mutex_lock(&cmd_mutex); + ret = send_at_command(at->cmd, at->cmd_len, + at->resp, sizeof(at->resp), + at->timeout_ms ? at->timeout_ms : + default_timeout_ms); + mutex_unlock(&cmd_mutex); + + if (ret >= 0) { + at->resp_len = ret; + /* Determine status from response */ + if (strnstr(at->resp, "OK", ret)) + at->status = 0; + else if (strnstr(at->resp, "+CME ERROR:", ret)) + at->status = -3; + else if (strnstr(at->resp, "+CMS ERROR:", ret)) + at->status = -3; + else if (strnstr(at->resp, "ERROR", ret)) + at->status = -1; + else + at->status = 0; /* Got data but no final result */ + } else if (ret == -ETIMEDOUT) { + at->resp_len = 0; + at->status = -2; + at->resp[0] = '\0'; + ret = 0; /* ioctl succeeded, timeout is in status */ + } else { + at->status = ret; + at->resp_len = 0; + } + + if (copy_to_user((void __user *)arg, at, sizeof(*at))) + ret = -EFAULT; + else + ret = 0; + + kfree(at); + break; + } + + case RC_SHANNON_GET_URC: { + struct rc_urc_msg msg; + + ret = urc_dequeue(&msg); + if (ret == -EAGAIN) { + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + if (wait_event_interruptible(urc_waitq, + urc_count() > 0)) + return -ERESTARTSYS; + ret = urc_dequeue(&msg); + if (ret) + return ret; + } + + if (copy_to_user((void __user *)arg, &msg, sizeof(msg))) + return -EFAULT; + ret = 0; + break; + } + + case RC_SHANNON_SET_TIMEOUT: { + int timeout; + + if (get_user(timeout, (int __user *)arg)) + return -EFAULT; + if (timeout < 100 || timeout > 60000) + return -EINVAL; + default_timeout_ms = timeout; + ret = 0; + break; + } + + case RC_SHANNON_GET_STATUS: { + struct rc_modem_status st; + + memset(&st, 0, sizeof(st)); + strscpy(st.device_path, modem_path_used, + sizeof(st.device_path)); + st.connected = (modem_filp && !IS_ERR(modem_filp)) ? 1 : 0; + st.urc_count = urc_count(); + st.cmds_sent = stat_cmds_sent; + st.cmds_failed = stat_cmds_failed; + st.bytes_tx = stat_bytes_tx; + st.bytes_rx = stat_bytes_rx; + + if (copy_to_user((void __user *)arg, &st, sizeof(st))) + return -EFAULT; + ret = 0; + break; + } + + case RC_SHANNON_FLUSH: { + unsigned long flags; + + spin_lock_irqsave(&urc_lock, flags); + urc_head = 0; + urc_tail = 0; + spin_unlock_irqrestore(&urc_lock, flags); + + resp_ready = false; + resp_len = 0; + ret = 0; + break; + } + + default: + ret = -ENOTTY; + } + + return ret; +} + static const struct file_operations rc_fops = { - .owner = THIS_MODULE, - .open = rc_open, - .release = rc_release, - .read = rc_read, - .write = rc_write, - .poll = rc_poll, + .owner = THIS_MODULE, + .open = rc_open, + .release = rc_release, + .read = rc_read, + .write = rc_write, + .poll = rc_poll, + .unlocked_ioctl = rc_ioctl, + .compat_ioctl = compat_ptr_ioctl, }; static int __init rc_shannon_init(void) @@ -242,8 +628,12 @@ static int __init rc_shannon_init(void) pr_err("rc_shannon: no modem device found — Shannon modem " "not present or not accessible\n"); modem_filp = NULL; - /* Don't fail — we'll create the device node anyway - * so userspace gets a clear error on read/write */ + /* + * Don't fail — create the device node anyway so userspace + * gets a clear -ENODEV on read/write rather than ENOENT + * on open. The modem may come up later (e.g., after SIM + * unlock or airplane mode toggle). + */ } /* Register char device */ @@ -252,7 +642,16 @@ static int __init rc_shannon_init(void) goto err_chrdev; major = MAJOR(dev); + /* + * class_create() signature changed in kernel 6.4: + * 6.4+: class_create(name) + * <6.4: class_create(THIS_MODULE, name) + */ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0) + rc_class = class_create(CLASS_NAME); +#else rc_class = class_create(THIS_MODULE, CLASS_NAME); +#endif if (IS_ERR(rc_class)) { ret = PTR_ERR(rc_class); goto err_class; @@ -272,9 +671,27 @@ static int __init rc_shannon_init(void) goto err_device; } - /* Make device world-accessible (root context anyway) */ pr_info("rc_shannon: /dev/%s created (major %d)\n", DEVICE_NAME, major); + + if (modem_filp) { + /* Verify modem is responsive */ + char test_resp[256]; + int test_ret; + + mutex_lock(&cmd_mutex); + test_ret = send_at_command("AT\r\n", 4, test_resp, + sizeof(test_resp), 2000); + mutex_unlock(&cmd_mutex); + + if (test_ret > 0 && strnstr(test_resp, "OK", test_ret)) + pr_info("rc_shannon: modem responsive (AT -> OK)\n"); + else + pr_warn("rc_shannon: modem opened but AT test " + "failed (ret=%d) — may need SELinux permissive\n", + test_ret); + } + return 0; err_device: @@ -299,7 +716,9 @@ static void __exit rc_shannon_exit(void) if (modem_filp) filp_close(modem_filp, NULL); - pr_info("rc_shannon: unloaded\n"); + pr_info("rc_shannon: unloaded — sent %llu commands, " + "%llu bytes tx, %llu bytes rx\n", + stat_cmds_sent, stat_bytes_tx, stat_bytes_rx); } module_init(rc_shannon_init); diff --git a/common/kmod/rc_wifi_mon.c b/common/kmod/rc_wifi_mon.c index a2e50be..e71b219 100644 --- a/common/kmod/rc_wifi_mon.c +++ b/common/kmod/rc_wifi_mon.c @@ -9,19 +9,19 @@ * * Supported drivers: * - Samsung SCSC/SLSI (scsc_wlan) - * - Broadcom bcmdhd / DHD - * - Qualcomm cnss2 / ath11k / ath12k (usually already has - * monitor, but vendor builds may disable it) + * - Broadcom bcmdhd / DHD (BCM4390 etc) + * - Qualcomm cnss2 / ath11k / ath12k * * Approach: * 1. Locate the WiFi driver's registered wiphy via cfg80211 * 2. Find the cfg80211_ops function table * 3. Patch change_virtual_intf to accept monitor mode * 4. Update wiphy->interface_modes bitmask - * 5. For SCSC: also hook the firmware command path to send - * the MIB key that enables RF monitor in Maxwell firmware + * 5. Driver-specific: send iovars/MIBs to firmware * * This is a live kernel patch — no reboot required after insmod. + * + * Target: Pixel 10 Pro Fold (rango), Tensor G5, BCM4390, kernel 6.6 */ #include @@ -30,8 +30,11 @@ #include #include #include -#include #include +#include +#include +#include +#include #include MODULE_LICENSE("GPL"); @@ -39,7 +42,32 @@ MODULE_AUTHOR("RadioControl"); MODULE_DESCRIPTION("Runtime WiFi monitor/injection mode enabler"); MODULE_VERSION("1.0"); -/* Which driver we detected */ +/* ---- kallsyms_lookup_name workaround for kernel >= 5.7 ---- */ + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0) +static unsigned long rc_kallsyms_lookup(const char *name) +{ + struct kprobe kp = { .symbol_name = name }; + unsigned long addr; + int ret; + + ret = register_kprobe(&kp); + if (ret < 0) + return 0; + addr = (unsigned long)kp.addr; + unregister_kprobe(&kp); + return addr; +} +#else +#include +static unsigned long rc_kallsyms_lookup(const char *name) +{ + return kallsyms_lookup_name(name); +} +#endif + +/* ---- Driver type detection ---- */ + enum wifi_driver_type { DRIVER_UNKNOWN = 0, DRIVER_SCSC, @@ -49,9 +77,15 @@ enum wifi_driver_type { DRIVER_CNSS, }; +static const char *driver_names[] = { + "unknown", "scsc", "bcmdhd", "ath11k", "ath12k", "cnss" +}; + static enum wifi_driver_type detected_driver = DRIVER_UNKNOWN; static struct wiphy *target_wiphy; -static struct cfg80211_ops *target_ops; +static const struct cfg80211_ops *target_ops; +static struct net_device *target_netdev; +static u32 orig_interface_modes; /* Original function pointer we're replacing */ static int (*orig_change_virtual_intf)(struct wiphy *wiphy, @@ -59,6 +93,156 @@ static int (*orig_change_virtual_intf)(struct wiphy *wiphy, enum nl80211_iftype type, struct vif_params *params); +/* ---- sysfs status interface ---- */ + +static struct kobject *rc_kobj; + +static ssize_t status_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, + "driver=%s\n" + "wiphy=%s\n" + "netdev=%s\n" + "monitor_supported=%d\n" + "interface_modes=0x%x\n", + driver_names[detected_driver], + target_wiphy ? wiphy_name(target_wiphy) : "none", + target_netdev ? target_netdev->name : "none", + target_wiphy ? !!(target_wiphy->interface_modes & + BIT(NL80211_IFTYPE_MONITOR)) : 0, + target_wiphy ? target_wiphy->interface_modes : 0); +} +static struct kobj_attribute status_attr = __ATTR_RO(status); + +static ssize_t driver_show(struct kobject *kobj, struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", driver_names[detected_driver]); +} +static struct kobj_attribute driver_attr = __ATTR_RO(driver); + +static struct attribute *rc_attrs[] = { + &status_attr.attr, + &driver_attr.attr, + NULL, +}; +static struct attribute_group rc_attr_group = { + .attrs = rc_attrs, +}; + +/* ---- bcmdhd driver-level monitor mode ---- */ + +/* + * BCM4390 uses the bcmdhd4390 driver. Monitor mode requires: + * 1. cfg80211 patching (we do above) so nl80211 accepts the type + * 2. DHD driver internal flag set via dhd_set_monitor() + * 3. Firmware iovar "monitor" set to 1 + * 4. Firmware iovar "promisc" set to 1 + * + * We locate dhd_net_if_lock/unlock and the iovar path via kallsyms. + * For full injection, Nexmon firmware patches are also required + * (separate from this module). + */ + +/* DHD iovar interface — we call dhd_iovar through the netdev private data */ +typedef int (*dhd_ioctl_fn)(struct net_device *dev, struct ifreq *ifr, int cmd); + +/* + * Send a bcmdhd private iovar via SIOCDEVPRIVATE. + * The DHD driver exposes iovars through wl_android_priv_cmd or + * through the standard SIOCDEVPRIVATE ioctl path. + * + * For monitor mode we need: + * wl monitor 1 (enable monitor) + * wl promisc 1 (promiscuous) + * wl allmulti 1 (all multicast) + */ +static int bcmdhd_set_iovar_int(const char *iovar, int val) +{ + typedef int (*wldev_iovar_setint_fn)(struct net_device *dev, + const char *iovar, int val); + wldev_iovar_setint_fn set_fn; + + set_fn = (wldev_iovar_setint_fn)rc_kallsyms_lookup("wldev_iovar_setint"); + if (!set_fn) { + pr_debug("rc_wifi_mon: wldev_iovar_setint not found\n"); + return -ENOSYS; + } + + if (!target_netdev) { + pr_err("rc_wifi_mon: no target netdev for iovar\n"); + return -ENODEV; + } + + return set_fn(target_netdev, iovar, val); +} + +static int bcmdhd_enable_monitor(void) +{ + int ret; + + ret = bcmdhd_set_iovar_int("monitor", 1); + if (ret) { + pr_warn("rc_wifi_mon: bcmdhd 'monitor' iovar failed: %d " + "(may need Nexmon firmware)\n", ret); + /* Non-fatal — cfg80211 patching still works for some captures */ + } else { + pr_info("rc_wifi_mon: bcmdhd firmware monitor mode enabled\n"); + } + + bcmdhd_set_iovar_int("promisc", 1); + bcmdhd_set_iovar_int("allmulti", 1); + + return ret; +} + +static void bcmdhd_disable_monitor(void) +{ + bcmdhd_set_iovar_int("monitor", 0); + bcmdhd_set_iovar_int("promisc", 0); + bcmdhd_set_iovar_int("allmulti", 0); +} + +/* ---- SCSC/SLSI firmware monitor mode ---- */ + +/* + * The Samsung SLSI firmware uses MIB OIDs to control behavior. + * We locate slsi_mlme_set() and write the monitor-enable MIB. + * + * Key MIBs for SCSC monitor mode: + * unifiMonitorModeEnabled = 0x09001E (OID) + * unifiRxDataRate = 0x090020 + * unifiFrameRxCounters = 0x090021 + * + * slsi_mlme_set signature: + * int slsi_mlme_set(struct slsi_dev *sdev, struct net_device *dev, + * u8 *mib, int mib_len); + * + * The MIB is TLV encoded: [OID 2B] [Length 2B] [Value...] + */ + +static void scsc_enable_fw_monitor(void) +{ + typedef int (*slsi_mlme_set_fn)(void *sdev, struct net_device *dev, + u8 *mib, int mib_len); + slsi_mlme_set_fn mlme_set; + + mlme_set = (slsi_mlme_set_fn)rc_kallsyms_lookup("slsi_mlme_set"); + if (!mlme_set) { + pr_info("rc_wifi_mon: slsi_mlme_set not found — " + "SCSC FW monitor mode MIB not set\n"); + pr_info("rc_wifi_mon: monitor mode will work at driver level " + "but FW may filter some frames\n"); + return; + } + + pr_info("rc_wifi_mon: found slsi_mlme_set, SCSC FW patching " + "available (MIB write deferred to mode switch)\n"); +} + +/* ---- cfg80211 ops patching ---- */ + /* * Our replacement change_virtual_intf that accepts monitor mode. * Falls through to the original handler for non-monitor types. @@ -68,28 +252,34 @@ static int rc_change_virtual_intf(struct wiphy *wiphy, enum nl80211_iftype type, struct vif_params *params) { - /* Allow monitor and OCB modes through */ if (type == NL80211_IFTYPE_MONITOR || type == NL80211_IFTYPE_OCB) { struct wireless_dev *wdev = dev->ieee80211_ptr; pr_info("rc_wifi_mon: setting interface %s to type %d\n", dev->name, type); - /* For monitor mode, we need to: - * 1. Bring the interface down - * 2. Change the type at the cfg80211 level - * 3. Set promiscuous mode on the hardware + /* + * Bring interface down before changing type. + * cfg80211 requires this for mode transitions. */ - if (netif_running(dev)) + if (netif_running(dev)) { + rtnl_lock(); dev_close(dev); + rtnl_unlock(); + } wdev->iftype = type; - /* Set flags for monitor mode */ if (type == NL80211_IFTYPE_MONITOR) { + /* Set radiotap header type for monitor mode */ + dev->type = ARPHRD_IEEE80211_RADIOTAP; + if (params && params->flags) wdev->u.mntr.flags = params->flags; - dev->type = ARPHRD_IEEE80211_RADIOTAP; + + /* Driver-specific firmware enable */ + if (detected_driver == DRIVER_BCMDHD) + bcmdhd_enable_monitor(); } else { dev->type = ARPHRD_ETHER; } @@ -97,6 +287,13 @@ static int rc_change_virtual_intf(struct wiphy *wiphy, return 0; } + /* Non-monitor type: if switching back from monitor, restore state */ + if (dev->ieee80211_ptr->iftype == NL80211_IFTYPE_MONITOR) { + dev->type = ARPHRD_ETHER; + if (detected_driver == DRIVER_BCMDHD) + bcmdhd_disable_monitor(); + } + /* All other types: pass through to original handler */ if (orig_change_virtual_intf) return orig_change_virtual_intf(wiphy, dev, type, params); @@ -105,8 +302,7 @@ static int rc_change_virtual_intf(struct wiphy *wiphy, } /* - * Detect which WiFi driver is active by checking module names - * and wiphy registration. + * Detect which WiFi driver is active by walking registered net devices. */ static enum wifi_driver_type detect_driver(void) { @@ -123,26 +319,32 @@ static enum wifi_driver_type detect_driver(void) if (strstr(drvname, "scsc") || strstr(drvname, "slsi")) { target_wiphy = dev->ieee80211_ptr->wiphy; + target_netdev = dev; rtnl_unlock(); return DRIVER_SCSC; } - if (strstr(drvname, "bcmdhd") || strstr(drvname, "dhd")) { + if (strstr(drvname, "bcmdhd") || strstr(drvname, "dhd") || + strstr(drvname, "bcm4")) { target_wiphy = dev->ieee80211_ptr->wiphy; + target_netdev = dev; rtnl_unlock(); return DRIVER_BCMDHD; } if (strstr(drvname, "ath11k")) { target_wiphy = dev->ieee80211_ptr->wiphy; + target_netdev = dev; rtnl_unlock(); return DRIVER_ATH11K; } if (strstr(drvname, "ath12k")) { target_wiphy = dev->ieee80211_ptr->wiphy; + target_netdev = dev; rtnl_unlock(); return DRIVER_ATH12K; } if (strstr(drvname, "cnss") || strstr(drvname, "qca")) { target_wiphy = dev->ieee80211_ptr->wiphy; + target_netdev = dev; rtnl_unlock(); return DRIVER_CNSS; } @@ -153,11 +355,20 @@ static enum wifi_driver_type detect_driver(void) const char *wname = wiphy_name(dev->ieee80211_ptr->wiphy); if (wname) { - if (strstr(wname, "scsc") || strstr(wname, "slsi")) { + if (strstr(wname, "scsc") || + strstr(wname, "slsi")) { target_wiphy = dev->ieee80211_ptr->wiphy; + target_netdev = dev; rtnl_unlock(); return DRIVER_SCSC; } + if (strstr(wname, "bcm") || + strstr(wname, "brcm")) { + target_wiphy = dev->ieee80211_ptr->wiphy; + target_netdev = dev; + rtnl_unlock(); + return DRIVER_BCMDHD; + } } } } @@ -167,8 +378,10 @@ static enum wifi_driver_type detect_driver(void) } /* - * Make a kernel text page writable so we can patch the ops table. - * We restore permissions after patching. + * Make a kernel text/rodata page writable so we can patch the ops table. + * Uses set_memory_rw/ro — safe on ARM64 with CONFIG_DEBUG_SET_MODULE_RONX=n. + * Falls back to direct write if page permissions can't be changed + * (module data sections are often already writable). */ static int make_ops_writable(void *addr, int writable) { @@ -185,103 +398,89 @@ static int make_ops_writable(void *addr, int writable) */ static int patch_wiphy(void) { + struct cfg80211_ops *ops_rw; + if (!target_wiphy) return -ENODEV; - target_ops = (struct cfg80211_ops *)target_wiphy->ops; + target_ops = target_wiphy->ops; if (!target_ops) return -ENODEV; - /* Save original handler */ + /* Save originals for restore on unload */ orig_change_virtual_intf = target_ops->change_virtual_intf; + orig_interface_modes = target_wiphy->interface_modes; - /* Add monitor mode to supported interface types */ + /* Add monitor and OCB to supported interface types */ target_wiphy->interface_modes |= BIT(NL80211_IFTYPE_MONITOR); target_wiphy->interface_modes |= BIT(NL80211_IFTYPE_OCB); - /* Patch the ops table */ - if (make_ops_writable((void *)target_ops, 1) == 0) { - ((struct cfg80211_ops *)target_ops)->change_virtual_intf = - rc_change_virtual_intf; - make_ops_writable((void *)target_ops, 0); - pr_info("rc_wifi_mon: patched change_virtual_intf\n"); + /* Patch the ops table — need to cast away const and make writable */ + ops_rw = (struct cfg80211_ops *)target_ops; + + if (make_ops_writable(ops_rw, 1) == 0) { + ops_rw->change_virtual_intf = rc_change_virtual_intf; + make_ops_writable(ops_rw, 0); + pr_info("rc_wifi_mon: patched change_virtual_intf via " + "set_memory_rw\n"); } else { - pr_warn("rc_wifi_mon: could not make ops writable, " - "trying direct write\n"); - /* Some kernels allow direct writes to module data sections */ - ((struct cfg80211_ops *)target_ops)->change_virtual_intf = - rc_change_virtual_intf; + /* + * set_memory_rw failed — try direct write. This works if + * the ops table lives in a writable module data section + * rather than .rodata. + */ + pr_info("rc_wifi_mon: set_memory_rw failed, attempting " + "direct patch\n"); + ops_rw->change_virtual_intf = rc_change_virtual_intf; } - pr_info("rc_wifi_mon: interface_modes now: 0x%x\n", - target_wiphy->interface_modes); + pr_info("rc_wifi_mon: interface_modes: 0x%x -> 0x%x\n", + orig_interface_modes, target_wiphy->interface_modes); return 0; } /* - * For SCSC/SLSI driver: send MIB keys to Maxwell firmware to - * enable raw frame reception in monitor mode. - * - * The SLSI firmware uses MIB OIDs to control behavior. Key MIBs: - * - unifiRxDataRate (for rate info in radiotap) - * - unifiTxDataConfirm (for TX status) - * - unifiMonitorModeEnabled (primary enable) - * - unifiFrameRxCounters (statistics) - * - * We locate slsi_mlme_set() via kallsyms and call it directly. + * Restore original state on module unload. */ -static void scsc_enable_fw_monitor(void) +static void unpatch_wiphy(void) { - typedef int (*slsi_mlme_set_fn)(void *sdev, void *dev, - u8 *mib, int mib_len); - slsi_mlme_set_fn mlme_set; + struct cfg80211_ops *ops_rw; - mlme_set = (slsi_mlme_set_fn)kallsyms_lookup_name("slsi_mlme_set"); - if (!mlme_set) { - pr_info("rc_wifi_mon: slsi_mlme_set not found — " - "SCSC FW monitor mode MIB not set\n"); - pr_info("rc_wifi_mon: monitor mode will work at driver level " - "but FW may filter some frames\n"); + if (!target_ops || !orig_change_virtual_intf) return; + + ops_rw = (struct cfg80211_ops *)target_ops; + + /* Restore original change_virtual_intf */ + if (make_ops_writable(ops_rw, 1) == 0) { + ops_rw->change_virtual_intf = orig_change_virtual_intf; + make_ops_writable(ops_rw, 0); + } else { + ops_rw->change_virtual_intf = orig_change_virtual_intf; } - pr_info("rc_wifi_mon: found slsi_mlme_set, SCSC FW patching " - "available (MIB write deferred to mode switch)\n"); - /* Actual MIB write happens when interface is switched to monitor — - * we hook into the mode change path above */ + /* Restore original interface modes */ + if (target_wiphy) + target_wiphy->interface_modes = orig_interface_modes; + + /* If interface is still in monitor mode, reset it */ + if (target_netdev && target_netdev->ieee80211_ptr && + target_netdev->ieee80211_ptr->iftype == NL80211_IFTYPE_MONITOR) { + target_netdev->ieee80211_ptr->iftype = NL80211_IFTYPE_STATION; + target_netdev->type = ARPHRD_ETHER; + if (detected_driver == DRIVER_BCMDHD) + bcmdhd_disable_monitor(); + } } -/* - * For bcmdhd: set the DHD driver's monitor mode flag and issue - * the firmware iovar to enable monitor. - */ -static void bcmdhd_prepare_monitor(void) -{ - /* The bcmdhd driver checks an internal flag before allowing - * monitor mode. We locate dhd_monitor_init or the cfg80211 - * vendor command handler. - * - * Key iovars we need the firmware to accept: - * - "monitor" (1 = enable) - * - "promisc" (1 = promiscuous) - * - "allmulti" (1 = all multicast) - * - * For full injection, the firmware needs to be patched - * (Nexmon-style). Our module enables the driver-level path; - * firmware patching is a separate step via the nexmon framework. - */ - pr_info("rc_wifi_mon: bcmdhd driver detected — driver-level " - "monitor mode enabled\n"); - pr_info("rc_wifi_mon: for packet injection, Nexmon firmware " - "patch is also required\n"); -} +/* ---- Module init/exit ---- */ static int __init rc_wifi_mon_init(void) { int ret; - pr_info("rc_wifi_mon: RadioControl WiFi monitor mode enabler\n"); + pr_info("rc_wifi_mon: RadioControl WiFi monitor mode enabler v1.0\n"); detected_driver = detect_driver(); if (detected_driver == DRIVER_UNKNOWN) { @@ -289,9 +488,10 @@ static int __init rc_wifi_mon_init(void) return -ENODEV; } - pr_info("rc_wifi_mon: detected driver type: %d, wiphy: %s\n", - detected_driver, - target_wiphy ? wiphy_name(target_wiphy) : "null"); + pr_info("rc_wifi_mon: detected driver: %s, wiphy: %s, netdev: %s\n", + driver_names[detected_driver], + target_wiphy ? wiphy_name(target_wiphy) : "null", + target_netdev ? target_netdev->name : "null"); ret = patch_wiphy(); if (ret) { @@ -305,32 +505,40 @@ static int __init rc_wifi_mon_init(void) scsc_enable_fw_monitor(); break; case DRIVER_BCMDHD: - bcmdhd_prepare_monitor(); + pr_info("rc_wifi_mon: BCM4390 driver patched — monitor mode " + "enabled at driver level\n"); + pr_info("rc_wifi_mon: for packet injection, Nexmon firmware " + "patch is also required\n"); break; default: break; } + /* Create sysfs entries under /sys/kernel/rc_wifi_mon/ */ + rc_kobj = kobject_create_and_add("rc_wifi_mon", kernel_kobj); + if (rc_kobj) { + ret = sysfs_create_group(rc_kobj, &rc_attr_group); + if (ret) { + kobject_put(rc_kobj); + rc_kobj = NULL; + pr_warn("rc_wifi_mon: sysfs creation failed: %d\n", ret); + /* Non-fatal */ + } + } + pr_info("rc_wifi_mon: loaded — monitor mode available via " - "'iw dev wlanX set type monitor'\n"); + "'iw dev %s set type monitor'\n", + target_netdev ? target_netdev->name : "wlanX"); return 0; } static void __exit rc_wifi_mon_exit(void) { - /* Restore original handler */ - if (target_ops && orig_change_virtual_intf) { - if (make_ops_writable((void *)target_ops, 1) == 0) { - ((struct cfg80211_ops *)target_ops)->change_virtual_intf = - orig_change_virtual_intf; - make_ops_writable((void *)target_ops, 0); - } - } + unpatch_wiphy(); - /* Remove monitor from interface_modes */ - if (target_wiphy) { - target_wiphy->interface_modes &= ~BIT(NL80211_IFTYPE_MONITOR); - target_wiphy->interface_modes &= ~BIT(NL80211_IFTYPE_OCB); + if (rc_kobj) { + sysfs_remove_group(rc_kobj, &rc_attr_group); + kobject_put(rc_kobj); } pr_info("rc_wifi_mon: unloaded — monitor mode disabled\n"); diff --git a/module.prop b/module.prop index 507cf58..57e5c69 100644 --- a/module.prop +++ b/module.prop @@ -1,6 +1,6 @@ id=radiocontrol name=RadioControl -version=v1.0.0 -versionCode=1 +version=v1.1.0 +versionCode=2 author=snake description=Factory field test menus, WiFi radio mode switching (monitor/injection/mesh), hidden radio features, engineering mode & system flags — all from a WebUI. diff --git a/system/bin/radiocontrol b/system/bin/radiocontrol index e4b4722..7f29df1 100755 --- a/system/bin/radiocontrol +++ b/system/bin/radiocontrol @@ -350,7 +350,7 @@ get_wifi_firmware() { local cur_nv=$(cat /sys/module/bcmdhd4390/parameters/nvram_path 2>/dev/null) local info=$(cat /sys/module/bcmdhd4390/parameters/info_string 2>/dev/null | sed 's/"/\\"/g' | tr '\n' ' ') - echo "${result}],\"current_fw\":\"$cur_fw\",\"current_nvram\":\"$cur_nv\",\"info\":\"$info\"}" + echo "{\"files\":${result}],\"current_fw\":\"$cur_fw\",\"current_nvram\":\"$cur_nv\",\"info\":\"$info\"}" } # Get CP (modem processor) debug info @@ -382,10 +382,14 @@ trigger_cp_crash() { # Carrier config — read current settings get_carrier_config() { local volte=$(settings get global enhanced_4g_mode_enabled 2>/dev/null) + local vonr=$(settings get global vonr_enabled 2>/dev/null) local wfc=$(settings get global wifi_calling_enabled 2>/dev/null) + local vt=$(settings get global vt_ims_enabled 2>/dev/null) + local apn=$(settings get global allow_adding_apns 2>/dev/null) + local nradv=$(getprop persist.vendor.radio.nr_advanced 2>/dev/null) local netsel=$(getprop persist.dbg.hide_preferred_network_type 2>/dev/null) - echo "{\"volte\":\"$volte\",\"wfc\":\"$wfc\",\"hide_network_type\":\"$netsel\"}" + echo "{\"volte\":\"$volte\",\"vonr\":\"$vonr\",\"wfc\":\"$wfc\",\"vt\":\"$vt\",\"apn\":\"$apn\",\"nradv\":\"$nradv\",\"hide_network_type\":\"$netsel\"}" } # Carrier config — set a carrier override @@ -593,8 +597,7 @@ handle_request() { "GET /api/wifi/info") send_response "200 OK" "text/plain" "$(get_wifi_details)" ;; "GET /api/wifi/params") send_response "200 OK" "application/json" "$(get_wifi_params)" ;; "GET /api/wifi/firmware") - local fwdata=$(get_wifi_firmware) - send_response "200 OK" "application/json" "{\"files\":$fwdata}" + send_response "200 OK" "application/json" "$(get_wifi_firmware)" ;; "GET /api/cp") send_response "200 OK" "application/json" "$(get_cp_debug)" ;; "GET /api/carrier/config") send_response "200 OK" "application/json" "$(get_carrier_config)" ;; diff --git a/uninstall.sh b/uninstall.sh index 89eecfb..84b5fb5 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -9,6 +9,11 @@ if [ -f "$PID_FILE" ]; then kill $(cat "$PID_FILE") 2>/dev/null fi +# Unload kernel modules +for mod in rc_wifi_mon rc_shannon_cmd rc_diag_bridge; do + lsmod 2>/dev/null | grep -q "$mod" && rmmod "$mod" 2>/dev/null +done + # Restore WiFi to managed mode for iface in wlan0 wlan1 wifi0; do if [ -d "/sys/class/net/$iface" ]; then diff --git a/webroot/js/app.js b/webroot/js/app.js index 137a09e..73cb1a5 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -532,7 +532,7 @@ async function loadWifi() { } // WiFi hardware info - const wifiText = await fetch('/api/wifi/info').then(r => r.text()).catch(() => ''); + const wifiText = await fetch('/api/wifi/info').then(r => r.ok ? r.text() : '').catch(() => ''); const wc = document.getElementById('wifi-hw-info'); if (wifiText.trim()) { const lines = wifiText.trim().split('\n').filter(l => l.includes('=')); @@ -576,7 +576,7 @@ function updateKmodBtn(id, loaded) { async function toggleKmod(mod) { const kmod = await api('/api/kmod'); - const key = mod.replace('rc_', '').replace('_cmd', '_cmd'); + const key = mod.replace('rc_', '').replace('_cmd', ''); const mapKey = mod === 'rc_wifi_mon' ? 'wifi_mon' : mod === 'rc_shannon_cmd' ? 'shannon_cmd' : 'diag_bridge'; const loaded = kmod && kmod[mapKey]; const action = loaded ? 'unload' : 'load'; @@ -683,7 +683,11 @@ async function loadCarrier() { const cc = await api('/api/carrier/config'); if (cc) { setToggle('toggle-volte', cc.volte); + setToggle('toggle-vonr', cc.vonr); setToggle('toggle-wfc', cc.wfc); + setToggle('toggle-vt', cc.vt); + setToggle('toggle-apn', cc.apn); + setToggle('toggle-nradv', cc.nradv); setToggle('toggle-nettype', cc.hide_network_type === '0' ? '1' : '0'); }