Compare commits

...

2 Commits

Author SHA1 Message Date
sssnake
070f8a4fb3 Add stealth mode: SUSFS path hiding, prop masking, kmod hiding
Stealth mode toggle in WebUI hides RadioControl from detection:
- resetprop -n for invisible prop changes
- SUSFS sus_path hiding for module dir, config dir, device nodes
- SUSFS sus_kstat to hide kernel modules from /proc/modules
- sysfs rc_wifi_mon status node hidden
- Requires KernelSU-Next with SUSFS enabled
2026-03-31 21:00:59 -07:00
sssnake
db07b4f7ef v1.1.0: Complete kernel modules, fix WebUI bugs
Kernel modules fully implemented for kernel 6.6/Tensor G5:
- rc_wifi_mon: kprobes kallsyms, bcmdhd iovar monitor/promisc/allmulti,
  sysfs status at /sys/kernel/rc_wifi_mon/, clean unpatch on unload
- rc_shannon_cmd: ioctl interface (AT_CMD, GET_URC, SET_TIMEOUT,
  GET_STATUS, FLUSH), URC ring buffer (64 entries), modem probe on init
- rc_diag_bridge: HDLC decode with CRC-16 validation, FTM ioctl,
  EFS read/write/stat/unlink, version query, subsystem dispatch
- rc_ioctl.h: shared userspace header for all ioctl definitions
- All modules handle class_create() API change in kernel 6.4+

WebUI fixes:
- Fix malformed WiFi firmware JSON output
- Add vonr/vt/apn/nradv to carrier config read endpoint
- Fix carrier toggle state loading in frontend
- Fix redundant replace in kmod toggle logic

Makefile: single-module build (MOD=), make package target
uninstall.sh: unload kernel modules before cleanup
2026-03-31 20:25:44 -07:00
12 changed files with 1703 additions and 205 deletions

View File

@@ -1,16 +1,35 @@
# RadioControl out-of-tree kernel modules # 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 KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
obj-$(CONFIG_RC_WIFI_MON) += rc_wifi_mon.o 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_SHANNON_CMD) += rc_shannon_cmd.o
obj-$(CONFIG_RC_DIAG_BRIDGE) += rc_diag_bridge.o
# Default: build all # Default: build all
CONFIG_RC_WIFI_MON ?= m CONFIG_RC_WIFI_MON ?= m
CONFIG_RC_DIAG_BRIDGE ?= m
CONFIG_RC_SHANNON_CMD ?= 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: all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
@@ -20,3 +39,15 @@ clean:
install: install:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules_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

View File

@@ -3,11 +3,13 @@
Out-of-tree kernel modules for enabling hardware features that are Out-of-tree kernel modules for enabling hardware features that are
compiled out of production Android kernels. compiled out of production Android kernels.
Target: Pixel 10 Pro Fold (rango), Tensor G5, kernel 6.6.102
## Build Requirements ## 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) - ARM64 cross-compiler (aarch64-linux-gnu-gcc)
- Device-specific kernel config - Device-specific kernel config (CONFIG_KPROBES=y, CONFIG_MODULES=y)
## Modules ## 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 monitor mode and packet injection on chipsets that have the capability
but disable it in their cfg80211 change_virtual_intf handler. but disable it in their cfg80211 change_virtual_intf handler.
Supports: Features:
- Samsung SCSC/SLSI (slsi_set_monitor_mode — re-enables compiled-out path) - Uses kprobes-based kallsyms lookup (works on kernel 5.7+)
- Broadcom bcmdhd (patches cfg80211_ops to allow NL80211_IFTYPE_MONITOR) - Patches wiphy->interface_modes bitmask for monitor + OCB
- Qualcomm ath11k/ath12k/cnss (typically already supports monitor, but - Driver-specific firmware iovars for BCM4390 (monitor, promisc, allmulti)
this bypasses vendor restrictions) - 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 Supports:
Creates /dev/rc_diag — a simplified userspace interface to the Qualcomm - Broadcom bcmdhd4390 (BCM4390, primary target)
DIAG subsystem that bypasses the standard diag driver's filtering. - Samsung SCSC/SLSI (Exynos WiFi)
Allows reading/writing NV items and sending FTM commands from userspace. - Qualcomm ath11k/ath12k/cnss
### rc_shannon_cmd.ko ### rc_shannon_cmd.ko
Creates /dev/rc_shannon — direct command interface to Samsung Shannon Creates /dev/rc_shannon — direct command interface to Samsung Shannon
modem bypassing RIL. Allows raw AT command passthrough and IPC message modem bypassing RIL. Allows raw AT command passthrough and IPC message
injection for band locking, NR mode control, and diagnostic readout. 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 ## Building
```bash ```bash
@@ -41,14 +76,23 @@ export CROSS_COMPILE=aarch64-linux-gnu-
export KERNEL_DIR=/path/to/kernel/source export KERNEL_DIR=/path/to/kernel/source
# Build all modules # Build all modules
make -C $KERNEL_DIR M=$(pwd) modules make
# Or build individually # Build a single module
make -C $KERNEL_DIR M=$(pwd) CONFIG_RC_WIFI_MON=m modules make MOD=rc_wifi_mon
# Package .ko files for the module zip
make package
# Clean
make clean
``` ```
## Runtime Loading ## Runtime Loading
Modules are loaded by the RadioControl service.sh based on detected chipset. Modules are loaded by RadioControl's service.sh based on detected chipset.
KernelSU module overlay places them in /vendor/lib/modules/ or loads The service automatically:
directly via insmod. 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

View File

@@ -16,6 +16,11 @@
* - FTM commands (subsys 0x4B, subsys_id 11) * - FTM commands (subsys 0x4B, subsys_id 11)
* - Log mask / message mask configuration * - Log mask / message mask configuration
* - Raw DIAG passthrough for advanced use * - 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 <linux/module.h> #include <linux/module.h>
@@ -30,6 +35,8 @@
#include <linux/wait.h> #include <linux/wait.h>
#include <linux/poll.h> #include <linux/poll.h>
#include <linux/ioctl.h> #include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/version.h>
MODULE_LICENSE("GPL"); MODULE_LICENSE("GPL");
MODULE_AUTHOR("RadioControl"); MODULE_AUTHOR("RadioControl");
@@ -37,29 +44,58 @@ MODULE_DESCRIPTION("Qualcomm DIAG protocol bridge for NV/EFS/FTM access");
MODULE_VERSION("1.0"); MODULE_VERSION("1.0");
#define DEVICE_NAME "rc_diag" #define DEVICE_NAME "rc_diag"
#define CLASS_NAME "radiocontrol" #define CLASS_NAME "radiocontrol_diag"
#define DIAG_BUF_SIZE 8192 #define DIAG_BUF_SIZE 8192
/* DIAG command codes */ /* DIAG command codes */
#define DIAG_NV_READ_F 0x26 #define DIAG_NV_READ_F 0x26
#define DIAG_NV_WRITE_F 0x27 #define DIAG_NV_WRITE_F 0x27
#define DIAG_STATUS_F 0x0C
#define DIAG_VERNO_F 0x00
#define DIAG_SUBSYS_CMD_F 0x4B #define DIAG_SUBSYS_CMD_F 0x4B
#define DIAG_LOG_CONFIG_F 0x73 #define DIAG_LOG_CONFIG_F 0x73
#define DIAG_MSG_CONFIG_F 0x7D #define DIAG_MSG_CONFIG_F 0x7D
#define DIAG_EXT_BUILD_ID_F 0x7C
/* DIAG subsystem IDs */ /* DIAG subsystem IDs */
#define DIAG_SUBSYS_FTM 11 #define DIAG_SUBSYS_FTM 11
#define DIAG_SUBSYS_EFS2 19 #define DIAG_SUBSYS_EFS2 19
#define DIAG_SUBSYS_PARAMS 37 #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 */ /* IOCTL commands */
#define RC_DIAG_MAGIC 'D' #define RC_DIAG_MAGIC 'D'
#define RC_DIAG_NV_READ _IOWR(RC_DIAG_MAGIC, 1, struct rc_nv_item) #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_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_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_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_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 */ /* NV item structure */
struct rc_nv_item { struct rc_nv_item {
@@ -69,7 +105,7 @@ struct rc_nv_item {
uint32_t data_len; uint32_t data_len;
}; };
/* Raw DIAG command */ /* Raw DIAG command — allocated on heap due to size */
struct rc_diag_raw { struct rc_diag_raw {
uint8_t cmd[DIAG_BUF_SIZE]; uint8_t cmd[DIAG_BUF_SIZE];
uint32_t cmd_len; uint32_t cmd_len;
@@ -77,12 +113,34 @@ struct rc_diag_raw {
uint32_t resp_len; 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 */ /* EFS operation */
struct rc_efs_op { struct rc_efs_op {
char path[256]; char path[256];
uint8_t data[4096]; uint8_t data[4096];
uint32_t data_len; uint32_t data_len;
int32_t status; 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;
}; };
/* /*
@@ -135,6 +193,7 @@ static const uint16_t crc16_table[256] = {
static uint16_t crc16_calc(const uint8_t *buf, int len) static uint16_t crc16_calc(const uint8_t *buf, int len)
{ {
uint16_t crc = 0xFFFF; uint16_t crc = 0xFFFF;
while (len--) while (len--)
crc = crc16_table[(crc ^ *buf++) & 0xFF] ^ (crc >> 8); crc = crc16_table[(crc ^ *buf++) & 0xFF] ^ (crc >> 8);
return ~crc & 0xFFFF; return ~crc & 0xFFFF;
@@ -148,6 +207,9 @@ static int hdlc_encode(const uint8_t *src, int src_len,
int pos = 0; int pos = 0;
int i; int i;
if (dst_size < src_len * 2 + 6)
return -ENOMEM;
crc = crc16_calc(src, src_len); crc = crc16_calc(src, src_len);
dst[pos++] = HDLC_FLAG; 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 */ /* Append CRC (little-endian) with escaping */
for (i = 0; i < 2 && pos < dst_size - 2; i++) { for (i = 0; i < 2 && pos < dst_size - 2; i++) {
uint8_t b = (crc >> (i * 8)) & 0xFF; uint8_t b = (crc >> (i * 8)) & 0xFF;
if (b == HDLC_FLAG || b == HDLC_ESC) { if (b == HDLC_FLAG || b == HDLC_ESC) {
dst[pos++] = HDLC_ESC; dst[pos++] = HDLC_ESC;
dst[pos++] = b ^ HDLC_ESC_MASK; dst[pos++] = b ^ HDLC_ESC_MASK;
@@ -176,6 +239,67 @@ static int hdlc_encode(const uint8_t *src, int src_len,
return pos; 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 int major;
static struct class *rc_class; static struct class *rc_class;
static struct cdev rc_cdev; static struct cdev rc_cdev;
@@ -185,50 +309,134 @@ static DEFINE_MUTEX(diag_mutex);
static struct file *open_diag_device(void) static struct file *open_diag_device(void)
{ {
static const char *diag_paths[] = {
"/dev/diag",
"/dev/diag_mdm",
NULL
};
struct file *f; struct file *f;
int i;
f = filp_open("/dev/diag", O_RDWR | O_NONBLOCK, 0); for (i = 0; diag_paths[i]; i++) {
if (!IS_ERR(f)) 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; return f;
}
}
pr_info("rc_diag: /dev/diag not available (%ld)\n", PTR_ERR(f));
return ERR_PTR(-ENODEV); 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, static int send_diag_cmd(const uint8_t *cmd, int cmd_len,
uint8_t *resp, int resp_size) 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; loff_t pos = 0;
ssize_t written, bytes_read; ssize_t written, bytes_read;
int hdlc_len; int hdlc_len;
int timeout_ms = 2000; int timeout_ms = 3000;
int elapsed = 0; int elapsed = 0;
int decoded_len;
if (!diag_filp || IS_ERR(diag_filp)) if (!diag_filp || IS_ERR(diag_filp))
return -ENODEV; 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 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 */ /* Send to DIAG */
written = kernel_write(diag_filp, hdlc_buf, hdlc_len, &pos); written = kernel_write(diag_filp, hdlc_tx, hdlc_len, &pos);
if (written < 0) kfree(hdlc_tx);
return written;
/* Read response */ if (written < 0) {
kfree(hdlc_rx);
return written;
}
/* Read response with timeout */
pos = 0; pos = 0;
while (elapsed < timeout_ms) { while (elapsed < timeout_ms) {
bytes_read = kernel_read(diag_filp, resp, resp_size, &pos); bytes_read = kernel_read(diag_filp, hdlc_rx,
if (bytes_read > 0) 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 bytes_read;
}
return decoded_len;
}
return decoded_len;
}
msleep(20); msleep(20);
elapsed += 20; elapsed += 20;
} }
kfree(hdlc_rx);
return -ETIMEDOUT; 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) static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{ {
int ret = 0; 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)); ret = send_diag_cmd(diag_cmd, 3, diag_resp, sizeof(diag_resp));
if (ret > 0) { if (ret > 0) {
/* Response: [0x26] [NV_ID] [status] [data...] */ /*
* 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.status = diag_resp[3] | (diag_resp[4] << 8);
nv.data_len = ret > 133 ? 128 : ret - 5; nv.data_len = (ret > 133) ? 128 : ret - 5;
if (nv.data_len > 0) if (nv.data_len > 0)
memcpy(nv.data, diag_resp + 5, nv.data_len); memcpy(nv.data, diag_resp + 5,
nv.data_len);
}
if (copy_to_user((void __user *)arg, &nv, sizeof(nv))) if (copy_to_user((void __user *)arg, &nv, sizeof(nv)))
ret = -EFAULT; ret = -EFAULT;
else 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, ret = send_diag_cmd(diag_cmd, 3 + nv.data_len,
diag_resp, sizeof(diag_resp)); diag_resp, sizeof(diag_resp));
if (ret > 0) { if (ret > 0) {
if (ret >= 5)
nv.status = diag_resp[3] | (diag_resp[4] << 8); nv.status = diag_resp[3] | (diag_resp[4] << 8);
else
nv.status = 0xFFFF;
if (copy_to_user((void __user *)arg, &nv, sizeof(nv))) if (copy_to_user((void __user *)arg, &nv, sizeof(nv)))
ret = -EFAULT; ret = -EFAULT;
else else
@@ -301,8 +522,12 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
case RC_DIAG_RAW_CMD: { case RC_DIAG_RAW_CMD: {
struct rc_diag_raw *raw; struct rc_diag_raw *raw;
raw = kmalloc(sizeof(*raw), GFP_KERNEL); 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))) { if (copy_from_user(raw, (void __user *)arg, sizeof(*raw))) {
kfree(raw); kfree(raw);
@@ -310,11 +535,18 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
break; 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, ret = send_diag_cmd(raw->cmd, raw->cmd_len,
raw->resp, sizeof(raw->resp)); raw->resp, sizeof(raw->resp));
if (ret > 0) { if (ret > 0) {
raw->resp_len = ret; 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; ret = -EFAULT;
else else
ret = 0; ret = 0;
@@ -323,6 +555,393 @@ static long rc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
break; 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: default:
ret = -ENOTTY; 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 = { static const struct file_operations rc_fops = {
.owner = THIS_MODULE, .owner = THIS_MODULE,
.unlocked_ioctl = rc_ioctl, .unlocked_ioctl = rc_ioctl,
.compat_ioctl = rc_ioctl, .compat_ioctl = compat_ptr_ioctl,
}; };
static int __init rc_diag_init(void) static int __init rc_diag_init(void)
@@ -356,30 +975,43 @@ static int __init rc_diag_init(void)
return ret; return ret;
major = MAJOR(dev); 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)) { if (IS_ERR(rc_class)) {
ret = PTR_ERR(rc_class); ret = PTR_ERR(rc_class);
goto err; goto err;
} }
cdev_init(&rc_cdev, &rc_fops); 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), rc_device = device_create(rc_class, NULL, MKDEV(major, 0),
NULL, DEVICE_NAME); NULL, DEVICE_NAME);
if (IS_ERR(rc_device)) { if (IS_ERR(rc_device)) {
ret = PTR_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; return 0;
err2: err_device:
cdev_del(&rc_cdev); cdev_del(&rc_cdev);
err_cdev:
class_destroy(rc_class); class_destroy(rc_class);
err: err:
unregister_chrdev_region(MKDEV(major, 0), 1); unregister_chrdev_region(MKDEV(major, 0), 1);
if (diag_filp)
filp_close(diag_filp, NULL);
return ret; return ret;
} }

106
common/kmod/rc_ioctl.h Normal file
View File

@@ -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 <linux/types.h>
#include <linux/ioctl.h>
/* ---- /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 */

View File

@@ -6,20 +6,23 @@
* read responses from Samsung Shannon modem, bypassing the RIL * read responses from Samsung Shannon modem, bypassing the RIL
* lock on /dev/umts_router0. * lock on /dev/umts_router0.
* *
* On Exynos: talks to Shannon via /dev/umts_atc0 or umts_router0 * On Tensor G5: Shannon 5400 (S5400BUNUELO), paths include
* On Tensor: same Shannon modem, paths may be /dev/nr_atc0 * /dev/umts_router, /dev/umts_atc0, /dev/nr_atc0
* *
* This module: * This module:
* 1. Opens the underlying modem char device from kernel space * 1. Opens the underlying modem char device from kernel space
* 2. Creates /dev/rc_shannon as a proxy with proper queuing * 2. Creates /dev/rc_shannon as a proxy with proper queuing
* 3. Multiplexes between RadioControl userspace and the modem * 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? * Why a kernel module instead of just opening the device from userspace?
* - The RIL daemon holds /dev/umts_router0 open exclusively * - The RIL daemon holds /dev/umts_router0 open exclusively
* - Even with root, opening it races with RIL and can crash the modem * - Even with root, opening it races with RIL and can crash the modem
* - This module uses a secondary AT channel (atc0/atc1) that RIL * - This module uses a secondary AT channel (atc0/atc1) that RIL
* doesn't claim, or creates a proper multiplexed path * doesn't claim, or creates a proper multiplexed path
*
* Target: Pixel 10 Pro Fold (rango), Tensor G5, kernel 6.6
*/ */
#include <linux/module.h> #include <linux/module.h>
@@ -35,6 +38,9 @@
#include <linux/poll.h> #include <linux/poll.h>
#include <linux/kthread.h> #include <linux/kthread.h>
#include <linux/delay.h> #include <linux/delay.h>
#include <linux/ioctl.h>
#include <linux/circ_buf.h>
#include <linux/version.h>
MODULE_LICENSE("GPL"); MODULE_LICENSE("GPL");
MODULE_AUTHOR("RadioControl"); MODULE_AUTHOR("RadioControl");
@@ -43,14 +49,54 @@ MODULE_VERSION("1.0");
#define DEVICE_NAME "rc_shannon" #define DEVICE_NAME "rc_shannon"
#define CLASS_NAME "radiocontrol" #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 */ /* Modem device paths to try, in order of preference */
static const char *modem_paths[] = { static const char *modem_paths[] = {
"/dev/umts_atc0", /* Secondary AT channel — not claimed by RIL */ "/dev/umts_atc0", /* Secondary AT channel — not claimed by RIL */
"/dev/umts_atc1", /* Tertiary AT channel */ "/dev/umts_atc1", /* Tertiary AT channel */
"/dev/nr_atc0", /* Tensor NR naming */ "/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 NULL
}; };
@@ -59,14 +105,124 @@ static struct class *rc_class;
static struct cdev rc_cdev; static struct cdev rc_cdev;
static struct device *rc_device; static struct device *rc_device;
static struct file *modem_filp; static struct file *modem_filp;
static char modem_path_used[128];
static DEFINE_MUTEX(cmd_mutex); static DEFINE_MUTEX(cmd_mutex);
static DECLARE_WAIT_QUEUE_HEAD(resp_waitq); static DECLARE_WAIT_QUEUE_HEAD(resp_waitq);
static DECLARE_WAIT_QUEUE_HEAD(urc_waitq);
/* Response buffer */ /* Response buffer for synchronous command/response */
static char resp_buf[BUF_SIZE]; static char resp_buf[RESP_BUF_SIZE];
static int resp_len; static int resp_len;
static bool resp_ready; 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. * 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++) { for (i = 0; modem_paths[i]; i++) {
f = filp_open(modem_paths[i], O_RDWR | O_NONBLOCK, 0); f = filp_open(modem_paths[i], O_RDWR | O_NONBLOCK, 0);
if (!IS_ERR(f)) { if (!IS_ERR(f)) {
strscpy(modem_path_used, modem_paths[i],
sizeof(modem_path_used));
pr_info("rc_shannon: opened modem device: %s\n", pr_info("rc_shannon: opened modem device: %s\n",
modem_paths[i]); modem_paths[i]);
return f; return f;
@@ -91,45 +249,103 @@ static struct file *open_modem_device(void)
/* /*
* Send an AT command to the modem and read the response. * 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, 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; loff_t pos = 0;
ssize_t written, bytes_read; ssize_t written, bytes_read;
int timeout_ms = 3000;
int elapsed = 0; int elapsed = 0;
int total_read = 0; int total_read = 0;
char line_buf[1024];
int line_pos = 0;
bool in_response = false;
if (!modem_filp || IS_ERR(modem_filp)) if (!modem_filp || IS_ERR(modem_filp))
return -ENODEV; return -ENODEV;
if (timeout_ms <= 0)
timeout_ms = default_timeout_ms;
/* Write command to modem */ /* Write command to modem */
written = kernel_write(modem_filp, cmd, cmd_len, &pos); written = kernel_write(modem_filp, cmd, cmd_len, &pos);
if (written < 0) { if (written < 0) {
pr_err("rc_shannon: write failed: %zd\n", written); pr_err("rc_shannon: write failed: %zd\n", written);
stat_cmds_failed++;
return written; return written;
} }
stat_bytes_tx += written;
stat_cmds_sent++;
/* Read response with timeout */ /* Read response with timeout, filtering URCs */
memset(response, 0, resp_size); memset(response, 0, resp_size);
pos = 0; pos = 0;
while (elapsed < timeout_ms && total_read < resp_size - 1) { while (elapsed < timeout_ms && total_read < resp_size - 1) {
bytes_read = kernel_read(modem_filp, response + total_read, char tmp[512];
resp_size - 1 - total_read, &pos);
bytes_read = kernel_read(modem_filp, tmp, sizeof(tmp), &pos);
if (bytes_read > 0) { if (bytes_read > 0) {
total_read += bytes_read; int i;
/* Check for final response */
if (strnstr(response, "\r\nOK\r\n", total_read) || stat_bytes_rx += bytes_read;
strnstr(response, "\r\nERROR\r\n", total_read) ||
strnstr(response, "\r\n+CME ERROR:", total_read)) for (i = 0; i < bytes_read; i++) {
break; 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 { } else {
msleep(50); /* Part of command response */
elapsed += 50; 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(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; return total_read;
} }
@@ -147,13 +363,16 @@ static int rc_release(struct inode *inode, struct file *file)
return 0; return 0;
} }
/*
* write() — send AT command, response available via read()
*/
static ssize_t rc_write(struct file *file, const char __user *buf, static ssize_t rc_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos) size_t count, loff_t *ppos)
{ {
char cmd_buf[BUF_SIZE]; char cmd_buf[CMD_BUF_SIZE];
int ret; int ret;
if (count >= BUF_SIZE) if (count >= CMD_BUF_SIZE - 2)
return -EINVAL; return -EINVAL;
if (copy_from_user(cmd_buf, buf, count)) 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); 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') { if (count >= 2 && cmd_buf[count-2] == '\r' && cmd_buf[count-1] == '\n') {
/* Already terminated */ /* Already terminated */
} else if (count >= 1 && cmd_buf[count-1] == '\r') { } else if (count >= 1 && cmd_buf[count-1] == '\r') {
cmd_buf[count] = '\n'; cmd_buf[count] = '\n';
count++; 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 { } else {
cmd_buf[count] = '\r'; cmd_buf[count] = '\r';
cmd_buf[count+1] = '\n'; 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'; 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) { if (ret >= 0) {
resp_len = ret; resp_len = ret;
resp_ready = true; resp_ready = true;
wake_up_interruptible(&resp_waitq); wake_up_interruptible(&resp_waitq);
} else {
resp_len = 0;
resp_ready = false;
} }
mutex_unlock(&cmd_mutex); 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, static ssize_t rc_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos) 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 (!resp_ready) {
if (file->f_flags & O_NONBLOCK) if (file->f_flags & O_NONBLOCK)
return -EAGAIN; return -EAGAIN;
wait_event_interruptible(resp_waitq, resp_ready); if (wait_event_interruptible(resp_waitq, resp_ready))
return -ERESTARTSYS;
} }
if (!resp_ready) if (!resp_ready)
@@ -209,17 +441,169 @@ static ssize_t rc_read(struct file *file, char __user *buf,
return to_copy; 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, &resp_waitq, wait);
poll_wait(file, &urc_waitq, wait);
if (resp_ready) if (resp_ready)
mask |= POLLIN | POLLRDNORM; mask |= EPOLLIN | EPOLLRDNORM;
if (urc_count() > 0)
mask |= EPOLLPRI; /* URCs available via ioctl */
return mask; 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 = { static const struct file_operations rc_fops = {
.owner = THIS_MODULE, .owner = THIS_MODULE,
.open = rc_open, .open = rc_open,
@@ -227,6 +611,8 @@ static const struct file_operations rc_fops = {
.read = rc_read, .read = rc_read,
.write = rc_write, .write = rc_write,
.poll = rc_poll, .poll = rc_poll,
.unlocked_ioctl = rc_ioctl,
.compat_ioctl = compat_ptr_ioctl,
}; };
static int __init rc_shannon_init(void) 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 " pr_err("rc_shannon: no modem device found — Shannon modem "
"not present or not accessible\n"); "not present or not accessible\n");
modem_filp = NULL; 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 */ /* Register char device */
@@ -252,7 +642,16 @@ static int __init rc_shannon_init(void)
goto err_chrdev; goto err_chrdev;
major = MAJOR(dev); 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); rc_class = class_create(THIS_MODULE, CLASS_NAME);
#endif
if (IS_ERR(rc_class)) { if (IS_ERR(rc_class)) {
ret = PTR_ERR(rc_class); ret = PTR_ERR(rc_class);
goto err_class; goto err_class;
@@ -272,9 +671,27 @@ static int __init rc_shannon_init(void)
goto err_device; goto err_device;
} }
/* Make device world-accessible (root context anyway) */
pr_info("rc_shannon: /dev/%s created (major %d)\n", pr_info("rc_shannon: /dev/%s created (major %d)\n",
DEVICE_NAME, major); 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; return 0;
err_device: err_device:
@@ -299,7 +716,9 @@ static void __exit rc_shannon_exit(void)
if (modem_filp) if (modem_filp)
filp_close(modem_filp, NULL); 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); module_init(rc_shannon_init);

View File

@@ -9,19 +9,19 @@
* *
* Supported drivers: * Supported drivers:
* - Samsung SCSC/SLSI (scsc_wlan) * - Samsung SCSC/SLSI (scsc_wlan)
* - Broadcom bcmdhd / DHD * - Broadcom bcmdhd / DHD (BCM4390 etc)
* - Qualcomm cnss2 / ath11k / ath12k (usually already has * - Qualcomm cnss2 / ath11k / ath12k
* monitor, but vendor builds may disable it)
* *
* Approach: * Approach:
* 1. Locate the WiFi driver's registered wiphy via cfg80211 * 1. Locate the WiFi driver's registered wiphy via cfg80211
* 2. Find the cfg80211_ops function table * 2. Find the cfg80211_ops function table
* 3. Patch change_virtual_intf to accept monitor mode * 3. Patch change_virtual_intf to accept monitor mode
* 4. Update wiphy->interface_modes bitmask * 4. Update wiphy->interface_modes bitmask
* 5. For SCSC: also hook the firmware command path to send * 5. Driver-specific: send iovars/MIBs to firmware
* the MIB key that enables RF monitor in Maxwell firmware
* *
* This is a live kernel patch — no reboot required after insmod. * This is a live kernel patch — no reboot required after insmod.
*
* Target: Pixel 10 Pro Fold (rango), Tensor G5, BCM4390, kernel 6.6
*/ */
#include <linux/module.h> #include <linux/module.h>
@@ -30,8 +30,11 @@
#include <linux/netdevice.h> #include <linux/netdevice.h>
#include <linux/rtnetlink.h> #include <linux/rtnetlink.h>
#include <linux/version.h> #include <linux/version.h>
#include <linux/kallsyms.h>
#include <linux/set_memory.h> #include <linux/set_memory.h>
#include <linux/kprobes.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
#include <linux/if_arp.h>
#include <net/cfg80211.h> #include <net/cfg80211.h>
MODULE_LICENSE("GPL"); MODULE_LICENSE("GPL");
@@ -39,7 +42,32 @@ MODULE_AUTHOR("RadioControl");
MODULE_DESCRIPTION("Runtime WiFi monitor/injection mode enabler"); MODULE_DESCRIPTION("Runtime WiFi monitor/injection mode enabler");
MODULE_VERSION("1.0"); 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 <linux/kallsyms.h>
static unsigned long rc_kallsyms_lookup(const char *name)
{
return kallsyms_lookup_name(name);
}
#endif
/* ---- Driver type detection ---- */
enum wifi_driver_type { enum wifi_driver_type {
DRIVER_UNKNOWN = 0, DRIVER_UNKNOWN = 0,
DRIVER_SCSC, DRIVER_SCSC,
@@ -49,9 +77,15 @@ enum wifi_driver_type {
DRIVER_CNSS, DRIVER_CNSS,
}; };
static const char *driver_names[] = {
"unknown", "scsc", "bcmdhd", "ath11k", "ath12k", "cnss"
};
static enum wifi_driver_type detected_driver = DRIVER_UNKNOWN; static enum wifi_driver_type detected_driver = DRIVER_UNKNOWN;
static struct wiphy *target_wiphy; 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 */ /* Original function pointer we're replacing */
static int (*orig_change_virtual_intf)(struct wiphy *wiphy, 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, enum nl80211_iftype type,
struct vif_params *params); 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. * Our replacement change_virtual_intf that accepts monitor mode.
* Falls through to the original handler for non-monitor types. * 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, enum nl80211_iftype type,
struct vif_params *params) struct vif_params *params)
{ {
/* Allow monitor and OCB modes through */
if (type == NL80211_IFTYPE_MONITOR || type == NL80211_IFTYPE_OCB) { if (type == NL80211_IFTYPE_MONITOR || type == NL80211_IFTYPE_OCB) {
struct wireless_dev *wdev = dev->ieee80211_ptr; struct wireless_dev *wdev = dev->ieee80211_ptr;
pr_info("rc_wifi_mon: setting interface %s to type %d\n", pr_info("rc_wifi_mon: setting interface %s to type %d\n",
dev->name, type); dev->name, type);
/* For monitor mode, we need to: /*
* 1. Bring the interface down * Bring interface down before changing type.
* 2. Change the type at the cfg80211 level * cfg80211 requires this for mode transitions.
* 3. Set promiscuous mode on the hardware
*/ */
if (netif_running(dev)) if (netif_running(dev)) {
rtnl_lock();
dev_close(dev); dev_close(dev);
rtnl_unlock();
}
wdev->iftype = type; wdev->iftype = type;
/* Set flags for monitor mode */
if (type == NL80211_IFTYPE_MONITOR) { if (type == NL80211_IFTYPE_MONITOR) {
/* Set radiotap header type for monitor mode */
dev->type = ARPHRD_IEEE80211_RADIOTAP;
if (params && params->flags) if (params && params->flags)
wdev->u.mntr.flags = 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 { } else {
dev->type = ARPHRD_ETHER; dev->type = ARPHRD_ETHER;
} }
@@ -97,6 +287,13 @@ static int rc_change_virtual_intf(struct wiphy *wiphy,
return 0; 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 */ /* All other types: pass through to original handler */
if (orig_change_virtual_intf) if (orig_change_virtual_intf)
return orig_change_virtual_intf(wiphy, dev, type, params); 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 * Detect which WiFi driver is active by walking registered net devices.
* and wiphy registration.
*/ */
static enum wifi_driver_type detect_driver(void) 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")) { if (strstr(drvname, "scsc") || strstr(drvname, "slsi")) {
target_wiphy = dev->ieee80211_ptr->wiphy; target_wiphy = dev->ieee80211_ptr->wiphy;
target_netdev = dev;
rtnl_unlock(); rtnl_unlock();
return DRIVER_SCSC; 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_wiphy = dev->ieee80211_ptr->wiphy;
target_netdev = dev;
rtnl_unlock(); rtnl_unlock();
return DRIVER_BCMDHD; return DRIVER_BCMDHD;
} }
if (strstr(drvname, "ath11k")) { if (strstr(drvname, "ath11k")) {
target_wiphy = dev->ieee80211_ptr->wiphy; target_wiphy = dev->ieee80211_ptr->wiphy;
target_netdev = dev;
rtnl_unlock(); rtnl_unlock();
return DRIVER_ATH11K; return DRIVER_ATH11K;
} }
if (strstr(drvname, "ath12k")) { if (strstr(drvname, "ath12k")) {
target_wiphy = dev->ieee80211_ptr->wiphy; target_wiphy = dev->ieee80211_ptr->wiphy;
target_netdev = dev;
rtnl_unlock(); rtnl_unlock();
return DRIVER_ATH12K; return DRIVER_ATH12K;
} }
if (strstr(drvname, "cnss") || strstr(drvname, "qca")) { if (strstr(drvname, "cnss") || strstr(drvname, "qca")) {
target_wiphy = dev->ieee80211_ptr->wiphy; target_wiphy = dev->ieee80211_ptr->wiphy;
target_netdev = dev;
rtnl_unlock(); rtnl_unlock();
return DRIVER_CNSS; return DRIVER_CNSS;
} }
@@ -153,11 +355,20 @@ static enum wifi_driver_type detect_driver(void)
const char *wname = wiphy_name(dev->ieee80211_ptr->wiphy); const char *wname = wiphy_name(dev->ieee80211_ptr->wiphy);
if (wname) { if (wname) {
if (strstr(wname, "scsc") || strstr(wname, "slsi")) { if (strstr(wname, "scsc") ||
strstr(wname, "slsi")) {
target_wiphy = dev->ieee80211_ptr->wiphy; target_wiphy = dev->ieee80211_ptr->wiphy;
target_netdev = dev;
rtnl_unlock(); rtnl_unlock();
return DRIVER_SCSC; 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. * Make a kernel text/rodata page writable so we can patch the ops table.
* We restore permissions after patching. * 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) 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) static int patch_wiphy(void)
{ {
struct cfg80211_ops *ops_rw;
if (!target_wiphy) if (!target_wiphy)
return -ENODEV; return -ENODEV;
target_ops = (struct cfg80211_ops *)target_wiphy->ops; target_ops = target_wiphy->ops;
if (!target_ops) if (!target_ops)
return -ENODEV; return -ENODEV;
/* Save original handler */ /* Save originals for restore on unload */
orig_change_virtual_intf = target_ops->change_virtual_intf; 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_MONITOR);
target_wiphy->interface_modes |= BIT(NL80211_IFTYPE_OCB); target_wiphy->interface_modes |= BIT(NL80211_IFTYPE_OCB);
/* Patch the ops table */ /* Patch the ops table — need to cast away const and make writable */
if (make_ops_writable((void *)target_ops, 1) == 0) { ops_rw = (struct cfg80211_ops *)target_ops;
((struct cfg80211_ops *)target_ops)->change_virtual_intf =
rc_change_virtual_intf; if (make_ops_writable(ops_rw, 1) == 0) {
make_ops_writable((void *)target_ops, 0); ops_rw->change_virtual_intf = rc_change_virtual_intf;
pr_info("rc_wifi_mon: patched change_virtual_intf\n"); make_ops_writable(ops_rw, 0);
pr_info("rc_wifi_mon: patched change_virtual_intf via "
"set_memory_rw\n");
} else { } else {
pr_warn("rc_wifi_mon: could not make ops writable, " /*
"trying direct write\n"); * set_memory_rw failed — try direct write. This works if
/* Some kernels allow direct writes to module data sections */ * the ops table lives in a writable module data section
((struct cfg80211_ops *)target_ops)->change_virtual_intf = * rather than .rodata.
rc_change_virtual_intf; */
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", pr_info("rc_wifi_mon: interface_modes: 0x%x -> 0x%x\n",
target_wiphy->interface_modes); orig_interface_modes, target_wiphy->interface_modes);
return 0; return 0;
} }
/* /*
* For SCSC/SLSI driver: send MIB keys to Maxwell firmware to * Restore original state on module unload.
* 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.
*/ */
static void scsc_enable_fw_monitor(void) static void unpatch_wiphy(void)
{ {
typedef int (*slsi_mlme_set_fn)(void *sdev, void *dev, struct cfg80211_ops *ops_rw;
u8 *mib, int mib_len);
slsi_mlme_set_fn mlme_set;
mlme_set = (slsi_mlme_set_fn)kallsyms_lookup_name("slsi_mlme_set"); if (!target_ops || !orig_change_virtual_intf)
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; 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 " /* Restore original interface modes */
"available (MIB write deferred to mode switch)\n"); if (target_wiphy)
/* Actual MIB write happens when interface is switched to monitor — target_wiphy->interface_modes = orig_interface_modes;
* we hook into the mode change path above */
/* 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();
}
} }
/* /* ---- Module init/exit ---- */
* 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");
}
static int __init rc_wifi_mon_init(void) static int __init rc_wifi_mon_init(void)
{ {
int ret; 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(); detected_driver = detect_driver();
if (detected_driver == DRIVER_UNKNOWN) { if (detected_driver == DRIVER_UNKNOWN) {
@@ -289,9 +488,10 @@ static int __init rc_wifi_mon_init(void)
return -ENODEV; return -ENODEV;
} }
pr_info("rc_wifi_mon: detected driver type: %d, wiphy: %s\n", pr_info("rc_wifi_mon: detected driver: %s, wiphy: %s, netdev: %s\n",
detected_driver, driver_names[detected_driver],
target_wiphy ? wiphy_name(target_wiphy) : "null"); target_wiphy ? wiphy_name(target_wiphy) : "null",
target_netdev ? target_netdev->name : "null");
ret = patch_wiphy(); ret = patch_wiphy();
if (ret) { if (ret) {
@@ -305,32 +505,40 @@ static int __init rc_wifi_mon_init(void)
scsc_enable_fw_monitor(); scsc_enable_fw_monitor();
break; break;
case DRIVER_BCMDHD: 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; break;
default: default:
break; 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 " 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; return 0;
} }
static void __exit rc_wifi_mon_exit(void) static void __exit rc_wifi_mon_exit(void)
{ {
/* Restore original handler */ unpatch_wiphy();
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);
}
}
/* Remove monitor from interface_modes */ if (rc_kobj) {
if (target_wiphy) { sysfs_remove_group(rc_kobj, &rc_attr_group);
target_wiphy->interface_modes &= ~BIT(NL80211_IFTYPE_MONITOR); kobject_put(rc_kobj);
target_wiphy->interface_modes &= ~BIT(NL80211_IFTYPE_OCB);
} }
pr_info("rc_wifi_mon: unloaded — monitor mode disabled\n"); pr_info("rc_wifi_mon: unloaded — monitor mode disabled\n");

View File

@@ -1,6 +1,6 @@
id=radiocontrol id=radiocontrol
name=RadioControl name=RadioControl
version=v1.0.0 version=v1.1.0
versionCode=1 versionCode=2
author=snake 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. description=Factory field test menus, WiFi radio mode switching (monitor/injection/mesh), hidden radio features, engineering mode & system flags — all from a WebUI.

View File

@@ -23,6 +23,7 @@ FACTORY_TEST_MODE=0
USB_DIAG_MODE=0 USB_DIAG_MODE=0
HIDDEN_MENUS=0 HIDDEN_MENUS=0
MODEM_LOG=0 MODEM_LOG=0
STEALTH_MODE=0
WIFI_MODE=managed WIFI_MODE=managed
# Kernel modules to load (space-separated) # Kernel modules to load (space-separated)
@@ -150,6 +151,41 @@ if [ "$DETECTED_SOC" = "tensor" ]; then
fi fi
fi fi
#############################
# Stealth mode
#############################
if [ "$STEALTH_MODE" = "1" ]; then
# Use resetprop -n to set props without triggering property_service
# This makes changes invisible to apps querying via __system_property_find
for prop in ro.build.type ro.debuggable ro.secure ro.adb.secure \
ro.factorytest persist.sys.factorytest; do
val=$(getprop "$prop" 2>/dev/null)
[ -n "$val" ] && resetprop -n "$prop" "$val" 2>/dev/null
done
# SUSFS path hiding — hide module directory from filesystem enumeration
# Requires KernelSU-Next with SUSFS enabled
if command -v ksud >/dev/null 2>&1; then
# Hide our module directory
ksud susfs add_sus_path "$MODDIR" 2>/dev/null
# Hide config directory
ksud susfs add_sus_path "$CONFIG_DIR" 2>/dev/null
# Hide kernel module device nodes
ksud susfs add_sus_path /dev/rc_shannon 2>/dev/null
ksud susfs add_sus_path /dev/rc_diag 2>/dev/null
# Hide sysfs status
ksud susfs add_sus_path /sys/kernel/rc_wifi_mon 2>/dev/null
fi
# Hide kernel modules from /proc/modules (SUSFS ksu_sus_kstat)
if [ -f /proc/susfs_sus_kstat ]; then
for mod in rc_wifi_mon rc_shannon_cmd rc_diag_bridge; do
echo "$mod" > /proc/susfs_sus_kstat 2>/dev/null
done
fi
fi
############################# #############################
# Mount debugfs if needed # Mount debugfs if needed
############################# #############################

View File

@@ -173,6 +173,7 @@ get_current_config() {
"usb_diag_mode": "${USB_DIAG_MODE:-0}", "usb_diag_mode": "${USB_DIAG_MODE:-0}",
"hidden_menus": "${HIDDEN_MENUS:-0}", "hidden_menus": "${HIDDEN_MENUS:-0}",
"modem_log": "${MODEM_LOG:-0}", "modem_log": "${MODEM_LOG:-0}",
"stealth_mode": "${STEALTH_MODE:-0}",
"wifi_mode": "${WIFI_MODE:-managed}", "wifi_mode": "${WIFI_MODE:-managed}",
"load_modules": "${LOAD_MODULES:-}", "load_modules": "${LOAD_MODULES:-}",
"detected_soc": "$soc" "detected_soc": "$soc"
@@ -252,7 +253,7 @@ update_config() {
local key="$1" val="$2" local key="$1" val="$2"
case "$key" in case "$key" in
ENGINEERING_MODE|FACTORY_TEST_MODE|USB_DIAG_MODE|HIDDEN_MENUS|MODEM_LOG) ENGINEERING_MODE|FACTORY_TEST_MODE|USB_DIAG_MODE|HIDDEN_MENUS|MODEM_LOG|STEALTH_MODE)
case "$val" in 0|1) ;; *) echo '{"ok":false,"error":"invalid value"}'; return ;; esac ;; case "$val" in 0|1) ;; *) echo '{"ok":false,"error":"invalid value"}'; return ;; esac ;;
WIFI_MODE) WIFI_MODE)
case "$val" in managed|monitor|injection|mesh|ap) ;; *) echo '{"ok":false,"error":"invalid mode"}'; return ;; esac ;; case "$val" in managed|monitor|injection|mesh|ap) ;; *) echo '{"ok":false,"error":"invalid mode"}'; return ;; esac ;;
@@ -350,7 +351,7 @@ get_wifi_firmware() {
local cur_nv=$(cat /sys/module/bcmdhd4390/parameters/nvram_path 2>/dev/null) 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' ' ') 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 # Get CP (modem processor) debug info
@@ -382,10 +383,14 @@ trigger_cp_crash() {
# Carrier config — read current settings # Carrier config — read current settings
get_carrier_config() { get_carrier_config() {
local volte=$(settings get global enhanced_4g_mode_enabled 2>/dev/null) 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 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) 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 # Carrier config — set a carrier override
@@ -593,8 +598,7 @@ handle_request() {
"GET /api/wifi/info") send_response "200 OK" "text/plain" "$(get_wifi_details)" ;; "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/params") send_response "200 OK" "application/json" "$(get_wifi_params)" ;;
"GET /api/wifi/firmware") "GET /api/wifi/firmware")
local fwdata=$(get_wifi_firmware) send_response "200 OK" "application/json" "$(get_wifi_firmware)"
send_response "200 OK" "application/json" "{\"files\":$fwdata}"
;; ;;
"GET /api/cp") send_response "200 OK" "application/json" "$(get_cp_debug)" ;; "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)" ;; "GET /api/carrier/config") send_response "200 OK" "application/json" "$(get_carrier_config)" ;;

View File

@@ -9,6 +9,11 @@ if [ -f "$PID_FILE" ]; then
kill $(cat "$PID_FILE") 2>/dev/null kill $(cat "$PID_FILE") 2>/dev/null
fi 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 # Restore WiFi to managed mode
for iface in wlan0 wlan1 wifi0; do for iface in wlan0 wlan1 wifi0; do
if [ -d "/sys/class/net/$iface" ]; then if [ -d "/sys/class/net/$iface" ]; then

View File

@@ -65,6 +65,13 @@
</div> </div>
<label class="toggle"><input type="checkbox" id="toggle-modem"><span class="toggle-slider"></span></label> <label class="toggle"><input type="checkbox" id="toggle-modem"><span class="toggle-slider"></span></label>
</div> </div>
<div class="card-row" style="border-top:2px solid var(--border)">
<div class="card-row-info">
<div class="card-row-label" style="color:var(--success)">Stealth Mode</div>
<div class="card-row-desc">Hide prop changes from detection, SUSFS hide module files and .ko modules</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-stealth"><span class="toggle-slider"></span></label>
</div>
</div> </div>
</div> </div>

View File

@@ -62,6 +62,7 @@ async function loadRadio() {
setToggle('toggle-hidden', config.hidden_menus); setToggle('toggle-hidden', config.hidden_menus);
setToggle('toggle-diag', config.usb_diag_mode); setToggle('toggle-diag', config.usb_diag_mode);
setToggle('toggle-modem', config.modem_log); setToggle('toggle-modem', config.modem_log);
setToggle('toggle-stealth', config.stealth_mode);
updateChips(); updateChips();
@@ -532,7 +533,7 @@ async function loadWifi() {
} }
// WiFi hardware info // 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'); const wc = document.getElementById('wifi-hw-info');
if (wifiText.trim()) { if (wifiText.trim()) {
const lines = wifiText.trim().split('\n').filter(l => l.includes('=')); const lines = wifiText.trim().split('\n').filter(l => l.includes('='));
@@ -576,7 +577,7 @@ function updateKmodBtn(id, loaded) {
async function toggleKmod(mod) { async function toggleKmod(mod) {
const kmod = await api('/api/kmod'); 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 mapKey = mod === 'rc_wifi_mon' ? 'wifi_mon' : mod === 'rc_shannon_cmd' ? 'shannon_cmd' : 'diag_bridge';
const loaded = kmod && kmod[mapKey]; const loaded = kmod && kmod[mapKey];
const action = loaded ? 'unload' : 'load'; const action = loaded ? 'unload' : 'load';
@@ -683,7 +684,11 @@ async function loadCarrier() {
const cc = await api('/api/carrier/config'); const cc = await api('/api/carrier/config');
if (cc) { if (cc) {
setToggle('toggle-volte', cc.volte); setToggle('toggle-volte', cc.volte);
setToggle('toggle-vonr', cc.vonr);
setToggle('toggle-wfc', cc.wfc); 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'); setToggle('toggle-nettype', cc.hide_network_type === '0' ? '1' : '0');
} }
@@ -810,6 +815,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('toggle-hidden')?.addEventListener('change', function() { handleToggle('HIDDEN_MENUS', this); }); document.getElementById('toggle-hidden')?.addEventListener('change', function() { handleToggle('HIDDEN_MENUS', this); });
document.getElementById('toggle-diag')?.addEventListener('change', function() { handleToggle('USB_DIAG_MODE', this); }); document.getElementById('toggle-diag')?.addEventListener('change', function() { handleToggle('USB_DIAG_MODE', this); });
document.getElementById('toggle-modem')?.addEventListener('change', function() { handleToggle('MODEM_LOG', this); }); document.getElementById('toggle-modem')?.addEventListener('change', function() { handleToggle('MODEM_LOG', this); });
document.getElementById('toggle-stealth')?.addEventListener('change', function() { handleToggle('STEALTH_MODE', this); });
// WiFi mode buttons // WiFi mode buttons
document.querySelectorAll('.mode-btn').forEach(btn => document.querySelectorAll('.mode-btn').forEach(btn =>