Simplify documentation and add USB kernel log module

This commit is contained in:
yusuf committed 2026-09-26 21:40:04 +03:00
1 parent 4537fb948f
commit d30c08392c
15 files changed
+548 -278

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
# Changelog
## Unreleased
- Standalone Quest Pro ARM64 kexec loader repository.
- Reproducible ECM-only BusyBox initramfs with DHCP and telnet shell.
- Device capture, payload preparation, hash-verified run and retained-log tools.
- Kernel patch series matching the maintained `qkx-kexec` branch.
- Validated with 8 CPUs, DRM, USB, QMP, syncboss, camera CPAS/CDM and SMB5.
-10
View File
@@ -1,10 +0,0 @@
# Contributing
Keep hardware/OS specifics explicit in commit messages. Do not commit boot
images, firmware, device captures, serial numbers, vbmeta digests, proprietary
binaries, or files pulled from partitions. Generated payloads belong under
`out/` and are ignored.
A hardware test report should state the Android build/kernel, target commit,
loader parameters, target command line additions, power/cable state, and whether
retained/pstore evidence survived.
+94
View File
@@ -0,0 +1,94 @@
# Build
There are two kernel builds:
- **Android kernel:** used to compile `quest_kexec.ko`
- **Target kernel:** the patched kernel booted by kexec
## Requirements
Install:
- Clang and LLD
- `aarch64-linux-gnu-` GCC/binutils
- `adb`, Python 3, `dtc`, `fdtput`, `curl`, `make`
## 1. Build the Android kernel tree
The loader must match the kernel reported by:
```sh
adb shell uname -r
```
For OS build `51503870024400340`, use Meta kernel commit:
```text
fa2e480a85c6adbcc73dfc4f0ab0728781e14584
```
Save the running config:
```sh
mkdir -p android-out
adb exec-out 'su -c "zcat /proc/config.gz"' > android-out/.config
```
Disable the automatic git suffix, then build `Image modules`. A full modules
build is needed for a correct `Module.symvers`.
```sh
SRC=/path/to/android-kernel-source
OUT=$PWD/android-out
$SRC/scripts/config --file "$OUT/.config" --disable LOCALVERSION_AUTO
make -C "$SRC" O="$OUT" ARCH=arm64 CC=clang LD=ld.lld HOSTCC=clang \
CROSS_COMPILE=aarch64-linux-gnu- CLANG_TRIPLE=aarch64-linux-gnu- \
LOCALVERSION=-g49638c7a8637 olddefconfig
make -C "$SRC" O="$OUT" ARCH=arm64 CC=clang LD=ld.lld HOSTCC=clang \
CROSS_COMPILE=aarch64-linux-gnu- CLANG_TRIPLE=aarch64-linux-gnu- \
LOCALVERSION=-g49638c7a8637 KCFLAGS=-I$SRC/drivers/pinctrl \
-j"$(nproc)" Image modules
```
## 2. Build the kexec and log modules
```sh
make module KDIR="$OUT" LOCALVERSION=-g49638c7a8637
modinfo -F vermagic module/quest_kexec.ko
```
The vermagic must start with the output of `adb shell uname -r`.
## 3. Build the target kernel
Use the patched kernel fork:
```sh
git clone https://github.com/YusufOzmen01/oculus-linux-kernel
cd oculus-linux-kernel
git checkout oculus-quest-pro-kernel-master
```
Build it using the Quest Pro config. The target needs at least:
```text
CONFIG_LOCALVERSION_AUTO=n
CONFIG_ION_POOL_AUTO_REFILL=n
CONFIG_USB_CONFIGFS_ECM=y
CONFIG_DEVTMPFS=y
CONFIG_PRINTK_TIME=y
CONFIG_LOG_BUF_SHIFT=20
```
The result must be a raw ARM64 `Image`.
## 4. Build the initramfs
```sh
make initramfs
```
This downloads and verifies BusyBox 1.36.1, builds it statically for ARM64 and
creates `out/initramfs.gz`.
+68
View File
@@ -0,0 +1,68 @@
# Port a custom Quest Pro kernel
This project is for Quest Pro only. Porting here means adapting another Quest
Pro 4.19 kernel release, not another headset or SoC.
## 1. Start from the matching Meta source
Find the Meta source commit for the headset OS version. Keep a clean copy for
building the Android loader module.
## 2. Apply the target patches
The maintained kernel fork already contains the changes. For another source
revision, apply the patches in order:
```sh
git am /path/to/quest-pro-kexec/kernel/patches/*.patch
```
Resolve conflicts by behavior, not by blindly choosing one side. The important
areas are:
- ARM64 entry and SMP/GIC state
- apps SMMU and msm_bus/RPMh
- display clocks/GDSCs/SDE
- DWC3 and USB PHY
- AOP QMP adoption
- SPMI stale IRQ cleanup
- camera CPAS/CDM
- SMB5 charger handoff
- retained log and boot watchdog
## 3. Keep the required target command line
`tools/prep.sh` adds:
```text
rdinit=/init nokaslr qkx_qmp_adopt=1
initcall_blacklist=virtual_sensor_driver_init
```
Do not use loader-side QMP disconnect. Taking the AP/AOP link down caused a PMIC
reset. The target must adopt the inherited link instead.
## 4. Keep the staging layout
```text
Image 0x90080000
initramfs 0x93000000
DTB 0x93200000
retained log 0x9ba80000 (64 KiB)
```
The Image memory size must be at most `0x2f80000`. Initramfs and DTB must each
fit in 2 MiB.
## 5. Test in small steps
First boot with the supplied RAM-only initramfs. Check:
```sh
getconf _NPROCESSORS_ONLN
ls /dev/dri
ip addr show usb0
dmesg | grep -E 'qkxqmp|QPNP SMB5|cam_hw_cdm_init'
```
Only move to a larger userspace after the basic target is repeatable.
+78
View File
@@ -0,0 +1,78 @@
# Use
## 1. Capture the running device state
Boot rooted Android and connect ADB:
```sh
tools/capture.sh out/captured
```
This reads the runtime device tree, iomem, command line and kernel config. It
does not modify the device.
Use the runtime tree from `/proc/device-tree`. The original
`/sys/firmware/fdt` blob is not safe for this handoff.
## 2. Prepare the payload
```sh
tools/prep.sh out/captured /path/to/target/Image \
out/initramfs.gz out/payload
```
## 3. Boot it
```sh
tools/run.sh out/payload
```
The script freshly pushes and verifies the module, Image, initramfs and DTB.
All device files are placed under `/data/local/tmp`.
## 4. Connect to the shell
Find the new Ethernet interface with `ip link`. Then:
```sh
nmcli device connect <interface>
telnet 10.42.0.2
```
If DHCP is unavailable:
```sh
sudo ip addr add 10.42.0.1/24 dev <interface>
telnet 10.42.0.2
```
Run one command without telnet:
```sh
python3 tools/qkx_sh.py 'uname -a'
```
## 5. Return to Android
```sh
python3 tools/qkx_sh.py 'echo r > /proc/qkx_bootdone'
```
## Logs
After Android returns:
```sh
tools/read-log.sh
```
This loads `marker_read.ko` and prints the target's retained RAM log.
To capture Android kernel messages over the existing USB gadget before kexec:
```sh
tools/load-usb-log.sh 60
```
`usb_log.ko` hooks the active gadget's EP0 callback and intentionally cannot be
unloaded safely. Reboot Android to remove it.
+48 -87
View File
@@ -1,101 +1,62 @@
# Quest Pro kexec
Experimental ARM64 kexec loader and RAM-only Linux environment for Meta Quest Pro
(Seacliff/Kona, Linux 4.19). It hands execution from the rooted Android kernel to
a patched Quest kernel entirely from RAM. No partition is flashed or mounted.
A RAM-only ARM64 kexec loader for Meta Quest Pro.
The included initramfs exposes a BusyBox shell over USB Ethernet (ECM):
It boots a patched Linux kernel from rooted Android without flashing a
partition. The included initramfs starts a BusyBox shell over USB Ethernet.
- target: `10.42.0.2`
- host: `10.42.0.1` (offered by target DHCP)
## Current status
Tested on Quest Pro OS build `51503870024400340`.
Working:
- all 8 CPUs
- DRM and both panels
- USB Ethernet
- AOP/QMP handoff
- syncboss
- camera CPAS/CDM
- SPMI and SMB5 charging
The patched kernel is in the `oculus-quest-pro-kernel-master` branch of:
https://github.com/YusufOzmen01/oculus-linux-kernel
The same changes are also included as patch files in `kernel/patches/`.
## Layout
- `module/` — kexec module and log modules
- `initramfs/` — BusyBox USB-network initramfs
- `tools/` — capture, prepare, run and shell scripts
- `kernel/patches/` — Quest Pro target-kernel changes
- `HOW_TO_BUILD.md` — build instructions
- `HOW_TO_USE.md` — boot and shell instructions
- `HOW_TO_PORT.md` — adapting another Quest Pro kernel
- `TROUBLESHOOTING.md` — common failures
## Network shell
The initramfs exposes one ECM USB function:
- headset: `10.42.0.2`
- host: `10.42.0.1`
- shell: `telnet 10.42.0.2`
- no USB serial functions and no block-device mounts
## Status
Validated on Quest Pro OS build `51503870024400340`, Android kernel
`4.19.325-cip128-st12-g49638c7a8637`, Meta source commit
`fa2e480a85c6adbcc73dfc4f0ab0728781e14584`.
Working in the RAM-only target: 8 CPUs, DRM/panels, USB ECM, AOP QMP handoff,
syncboss, camera CPAS/CDM, SPMI and SMB5 charging. The target kernel patches are
also maintained on the `qkx-kexec` branch of
[`YusufOzmen01/oculus-linux-kernel`](https://github.com/YusufOzmen01/oculus-linux-kernel).
Patch files are mirrored under `kernel/patches/` for review and portability.
The target runs DHCP, so NetworkManager can normally configure the host
interface automatically.
## Safety
This is research software. A failed handoff can freeze or reboot the headset.
Keep the stock boot image available and use only on a rooted device you can
recover. The supplied scripts only write temporary files under
`/data/local/tmp`; they do not flash, format or mount partitions.
This is experimental. A bad handoff can reboot or lock the headset.
Do **not** use a loader module built for a different running Android kernel.
Module ABI mismatches can crash Android during an irreversible shutdown.
The included scripts only copy files to `/data/local/tmp`. They do not flash,
format or mount partitions.
## Host requirements
Linux host with:
- `adb`, root access through `adb shell 'su -c ...'`
- Clang/LLD and an `aarch64-linux-gnu-` toolchain
- Python 3, `dtc`, `fdtput`, `curl`, `make`, `cpio`, `gzip`
- NetworkManager or another way to configure the ECM host interface
## Quick start
See [docs/build.md](docs/build.md) for complete kernel/module build steps.
Assuming the patched target `Image` and matching modules already exist:
```sh
# Build static BusyBox and the ECM-only initramfs
make initramfs
# Build loader against the *running Android kernel's* completed build tree
make module KDIR=/path/to/android-kernel-out \
LOCALVERSION=-g49638c7a8637
# Capture runtime DT, iomem, cmdline, config (read-only on device)
tools/capture.sh out/captured
# Prepare a payload using the patched target kernel
tools/prep.sh out/captured /path/to/target/Image \
out/initramfs.gz out/payload
# Freshly pushes and verifies every file before jumping
tools/run.sh out/payload
```
After USB re-enumerates, NetworkManager should obtain `10.42.0.1` from the
target. If not:
```sh
nmcli device connect <ECM-interface>
# or: sudo ip addr add 10.42.0.1/24 dev <ECM-interface>
telnet 10.42.0.2
```
Return to Android while the target is running:
```sh
python3 tools/qkx_sh.py 'echo r > /proc/qkx_bootdone'
```
After Android returns, retrieve a retained target log:
```sh
tools/read-log.sh
```
## Repository layout
- `module/` — kexec loader, ARM64 transition code, retained-log reader
- `initramfs/` — ECM-only PID 1 and reproducible BusyBox/initramfs builders
- `tools/` — device capture, payload preparation, run and shell helpers
- `kernel/patches/` — patches required by the target kernel
- `docs/` — detailed build, usage and architecture notes
Always build `quest_kexec.ko` against the exact Android kernel currently running
on the headset. Never force-load a module with the wrong ABI.
## License
GPL-2.0. See `LICENSE`. BusyBox is downloaded from upstream and built locally;
its binary and source are not committed.
GPL-2.0. See `LICENSE`.
-8
View File
@@ -1,8 +0,0 @@
# Security
The loader deliberately crosses a kernel trust boundary and executes a second
kernel without a bootloader verification step. Use it only on hardware you own.
Do not distribute boot images, firmware, device keys or captures.
Report security-sensitive issues privately to the repository owner rather than
posting device identifiers or logs containing secrets in a public issue.
+40
View File
@@ -0,0 +1,40 @@
# Troubleshooting
## Module refuses to load
Check:
```sh
adb shell uname -r
modinfo -F vermagic module/quest_kexec.ko
```
Rebuild against the exact Android source, config, local version and
`Module.symvers`. Do not force-load it.
## USB appears but the shell is unreachable
The ECM-only interface may have a different name than older ACM+ECM payloads.
Find it with `ip link`, then use NetworkManager or assign `10.42.0.1/24`.
## Android returns immediately
Run:
```sh
tools/read-log.sh
```
A retained log means the target reached the log ring. Pstore may instead show a
loader panic inside Android.
## No retained log
A zero marker can mean a PMIC hard reset or a failure before the target log was
initialized. Do not assume the last target driver caused it without a logged
checkpoint.
## Device locks up
Force reboot only when necessary; it usually erases the useful RAM log. Do not
repeat the same payload blindly. Add a checkpoint or narrow the boundary first.
-50
View File
@@ -1,50 +0,0 @@
# Architecture and handoff notes
## Memory layout
| Payload | Physical address |
|---|---:|
| target Image | `0x90080000` |
| initramfs | `0x93000000` |
| DTB | `0x93200000` |
| retained ring | `0x9ba80000`, 64 KiB |
The loader stages segments in ordinary pages, freezes Android userspace,
quiesces GPU, syncboss and DWC3, calls the normal device shutdown path, offlines
secondary CPUs, flushes RPMh, copies the payload through an identity-mapped
ARM64 transition and enters the target at EL1.
## Important inherited-state fixes
The target patch series contains the fixes established during bring-up:
- SMP/GIC and apps-SMMU inherited-state tolerance
- msm_bus/RPMh QoS handling (Quest's nonexistent UFS-card master is skipped)
- display GDSCs, Display CC, SDE RSC and DRM handoff
- DWC3/USB PHY gadget handoff
- AOP QMP link adoption and inherited QDSS vote adoption
- syncboss quiesce through Android's PM callback
- SPMI stale interrupt disable
- CPAS/CCI child-node lookups and camera/CDM diagnostics
- SMB5 probe with unsafe synthetic initial IRQ callbacks skipped
- retained log plus boot watchdog diagnostics
## Runtime tree requirement
Use the runtime tree reconstructed from `/proc/device-tree`, not the original
blob in `/sys/firmware/fdt`. The original boot FDT caused a cold reset in a
validated comparison test. `tools/capture.sh` handles the correct conversion.
## QMP
Never take the AP-to-AOP link down during handoff. Loader-side QMP disconnect
caused an immediate PMIC reset. The target adopts the inherited physical link,
recycles only the logical channel and imports the negotiated mailbox metadata.
The required target command line option is `qkx_qmp_adopt=1`.
## SMB5
Android's normal `device_shutdown()` already calls `smb5_shutdown()`. The target
must not manually call the 11 synthetic initial-status interrupt handlers after
kexec; they reset or lock the PMIC/USB-C path. The patched driver defaults to
skipping those calls. Charging and real plug interrupts remain functional.
-70
View File
@@ -1,70 +0,0 @@
# Building
There are two kernels involved:
1. **Android kernel** currently running on the headset. It is used only to build
`quest_kexec.ko`; source, configuration, release suffix and `Module.symvers`
must match the device exactly.
2. **Target kernel** entered by kexec. It is the Meta Quest Pro kernel with the
patches in `kernel/patches/` (or branch `qkx-kexec` in the companion fork).
## Android loader ABI tree
For OS build `51503870024400340`, use Meta commit `fa2e480a85...`, copy the
headset's `/proc/config.gz` to the output `.config`, disable
`CONFIG_LOCALVERSION_AUTO`, and build `Image modules` with local version
`-g49638c7a8637`. A full module build is required to produce correct symbol
CRCs in `Module.symvers`.
The vendor audio tree needs its pinctrl include directory in `KCFLAGS`:
```sh
SRC=/path/to/meta-kernel-at-fa2e480a85
OUT=/path/to/android-out
adb exec-out 'su -c "zcat /proc/config.gz"' > "$OUT/.config"
$SRC/scripts/config --file "$OUT/.config" --disable LOCALVERSION_AUTO
make -C "$SRC" O="$OUT" ARCH=arm64 CC=clang LD=ld.lld HOSTCC=clang \
CROSS_COMPILE=aarch64-linux-gnu- CLANG_TRIPLE=aarch64-linux-gnu- \
LOCALVERSION=-g49638c7a8637 olddefconfig
make -C "$SRC" O="$OUT" ARCH=arm64 CC=clang LD=ld.lld HOSTCC=clang \
CROSS_COMPILE=aarch64-linux-gnu- CLANG_TRIPLE=aarch64-linux-gnu- \
LOCALVERSION=-g49638c7a8637 KCFLAGS=-I$SRC/drivers/pinctrl -j8 Image modules
make module KDIR="$OUT" LOCALVERSION=-g49638c7a8637
modinfo -F vermagic module/quest_kexec.ko
```
The reported vermagic must begin with the output of `adb shell uname -r`.
## Target kernel
Clone `YusufOzmen01/oculus-linux-kernel`, then check out `qkx-kexec`. Or apply
all patches in order to upstream Quest Pro commit `42dc87a978`:
```sh
git am /path/to/quest-pro-kexec/kernel/patches/*.patch
```
Start from the device config and make these configuration changes:
```text
CONFIG_LOCALVERSION_AUTO=n
CONFIG_ION_POOL_AUTO_REFILL=n
CONFIG_USB_CONFIGFS_ECM=y
CONFIG_DEVTMPFS=y
CONFIG_PRINTK_TIME=y
CONFIG_LOG_BUF_SHIFT=20
```
Build a raw ARM64 `Image`. The repository's staging layout requires text offset
`0x80000`, a relocatable little-endian 4 KiB-page image, maximum memory size
`0x2f80000`, and maximum initramfs/DTB size 2 MiB each.
## BusyBox/initramfs
```sh
make initramfs
```
`initramfs/busybox.sh` downloads BusyBox 1.36.1 from busybox.net, verifies its
published SHA-256, then cross-builds it static. `initramfs/build.py` verifies the
binary is static ARM64 and generates deterministic gzip/newc output.
-44
View File
@@ -1,44 +0,0 @@
# Usage and troubleshooting
## Normal run
1. Charge the headset and boot rooted Android.
2. Build loader for that exact Android kernel.
3. Build patched target kernel and initramfs.
4. Run `tools/capture.sh out/captured` after every OS/device-tree change.
5. Prepare and execute as shown in the README.
Each execution freshly pushes Image, initramfs and DTB, runs `sync` twice and
verifies MD5 hashes before inserting the loader.
## Target shell
The initramfs advertises one USB ECM function (VID:PID `1d6b:0105`) with fixed
MAC addresses. The interface name differs from old ACM+ECM payloads because ECM
is now interface zero. Prefer finding it by MAC or `ip link` rather than hard
coding its name.
`udhcpd` offers only `10.42.0.1/24`; it advertises no gateway or DNS, so the host
should not route Internet traffic through the headset.
## Reset classification
- Retained marker exists: target reached far enough to initialize the ring and
returned via a DRAM-preserving warm reset.
- Marker is zero and pstore contains `quest_kexec: native CPU shutdown failed`:
loader panic inside Android.
- Marker is zero with no pstore evidence: PMIC hard/cold reset or very early
failure; do not attribute it to the last target probe without a checkpoint.
`tools/read-log.sh` reads both retained-ring and pstore evidence after Android
returns. A stale marker is cleared before every run.
## Common failures
- **Module won't load / disagrees about symbol version:** rebuild against the
exact running Android source/config/release; never force-load it.
- **Gadget exists but target is unreachable:** ask NetworkManager to connect or
manually assign `10.42.0.1/24` to the new ECM interface.
- **No gadget and Android returns:** retrieve retained logs immediately.
- **Hard lockup:** force reboot may erase all evidence. Avoid repeating an
unchanged payload; add a checkpoint or narrow the boundary first.
+1
View File
@@ -6,6 +6,7 @@
# the headset is running, so Module.symvers carries the right CRCs.
ifneq ($(KERNELRELEASE),)
obj-m += quest_kexec.o marker_read.o
obj-m += usb_log.o
quest_kexec-y := loader.o transition.o
# Without this define the module can only stage (execute=1 is refused).
ccflags-y += -DQKX_ENABLE_EXPERIMENTAL_JUMP
+142
View File
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: GPL-2.0
/* Diagnostic console via vendor EP0 reads on the existing USB gadget.
* No exit function: the hooked callback must remain resident until reboot.
*/
#include <linux/module.h>
#include <linux/console.h>
#include <linux/kprobes.h>
#include <linux/slab.h>
#include <linux/usb/gadget.h>
#define RING_SIZE 65536
#define TRANSFER_SIZE 1024
/* Private UDC layout from this board's drivers/usb/gadget/udc/core.c. */
struct qlog_udc {
struct usb_gadget_driver *driver;
struct usb_gadget *gadget;
struct device dev;
struct list_head list;
bool vbus;
};
static unsigned long (*lookup)(const char *);
static int (*original_setup)(struct usb_gadget *, const struct usb_ctrlrequest *);
static struct usb_request *request;
static unsigned char ring[RING_SIZE];
static unsigned int head, tail;
static DEFINE_SPINLOCK(ring_lock);
static atomic_t dropped = ATOMIC_INIT(0);
static void log_write(struct console *con, const char *s, unsigned int count)
{
unsigned long flags;
unsigned int i;
if (!spin_trylock_irqsave(&ring_lock, flags)) {
atomic_add(count, &dropped);
return;
}
for (i = 0; i < count; i++) {
if (head - tail == RING_SIZE) {
tail++;
atomic_inc(&dropped);
}
ring[head++ & (RING_SIZE - 1)] = s[i];
}
spin_unlock_irqrestore(&ring_lock, flags);
}
static struct console log_console = {
.name = "qkxusb", .write = log_write, .flags = CON_ENABLED, .index = -1,
};
static void log_complete(struct usb_ep *ep, struct usb_request *req) { }
static int log_setup(struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
{
unsigned long flags;
unsigned int n = 0, size;
u8 *out = request->buf;
/* Magic request namespace: vendor/device/IN, request 0x5a,
* value 0x514b, index 0x584c. All other requests pass through unchanged.
* Reply is LE32 cumulative dropped bytes followed by console text.
*/
if (ctrl->bRequestType != 0xc0 || ctrl->bRequest != 0x5a ||
le16_to_cpu(ctrl->wValue) != 0x514b ||
le16_to_cpu(ctrl->wIndex) != 0x584c)
return original_setup(gadget, ctrl);
size = min_t(unsigned int, le16_to_cpu(ctrl->wLength), TRANSFER_SIZE);
if (size < 4)
return -EINVAL;
spin_lock_irqsave(&ring_lock, flags);
while (head != tail && n < size - 4)
out[4 + n++] = ring[tail++ & (RING_SIZE - 1)];
spin_unlock_irqrestore(&ring_lock, flags);
out[0] = atomic_read(&dropped);
out[1] = atomic_read(&dropped) >> 8;
out[2] = atomic_read(&dropped) >> 16;
out[3] = atomic_read(&dropped) >> 24;
request->length = n + 4;
request->zero = request->length < le16_to_cpu(ctrl->wLength);
return usb_ep_queue(gadget->ep0, request, GFP_ATOMIC);
}
static int match(struct device *dev, const void *name)
{
return !strcmp(dev_name(dev), name);
}
static int __init log_init(void)
{
struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
struct class **class;
struct device *dev;
struct qlog_udc *udc;
struct device *(*find)(struct class *, struct device *, const void *,
int (*)(struct device *, const void *));
void (*register_con)(struct console *);
struct mutex *udc_lock;
int ret = register_kprobe(&kp);
if (ret)
return ret;
lookup = (void *)kp.addr;
unregister_kprobe(&kp);
class = (void *)lookup("udc_class");
udc_lock = (void *)lookup("udc_lock");
find = (void *)lookup("class_find_device");
register_con = (void *)lookup("register_console");
if (!class || !*class || !find || !register_con || !udc_lock)
return -ENOENT;
dev = find(*class, NULL, "a600000.dwc3", match);
if (!dev)
return -ENODEV;
udc = container_of(dev, struct qlog_udc, dev);
mutex_lock(udc_lock);
if (!udc->driver || !udc->gadget || !udc->driver->setup ||
udc->gadget->dev.driver != &udc->driver->driver) {
ret = -EINVAL;
goto out;
}
request = usb_ep_alloc_request(udc->gadget->ep0, GFP_KERNEL);
if (!request) {
ret = -ENOMEM;
goto out;
}
request->buf = kmalloc(TRANSFER_SIZE, GFP_KERNEL);
if (!request->buf) {
usb_ep_free_request(udc->gadget->ep0, request);
ret = -ENOMEM;
goto out;
}
request->complete = log_complete;
original_setup = udc->driver->setup;
/* Publish fully initialized state before USB IRQs can enter our code. */
smp_wmb();
WRITE_ONCE(udc->driver->setup, log_setup);
mutex_unlock(udc_lock);
/* Keep the UDC device reference for the lifetime of this resident module. */
register_con(&log_console);
pr_info("qkx_usb_log: resident EP0 console ready; ring=%u\n", RING_SIZE);
return 0;
out:
mutex_unlock(udc_lock);
put_device(dev);
return ret;
}
module_init(log_init);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Resident Quest kernel console over USB vendor control reads");
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Load the Android-side USB console logger and read it from the host.
# This module intentionally cannot be unloaded; reboot Android to remove it.
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
SECONDS=${1:-60}
adb push "$HERE/module/usb_log.ko" /data/local/tmp/qkx_usb_log.ko >/dev/null
adb shell 'su -c "insmod /data/local/tmp/qkx_usb_log.ko"'
exec python3 "$HERE/tools/usb_log_host.py" --seconds "$SECONDS"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Read the Quest's resident EP0 console; no ADB, interface claim or detach.
Uses libusb through ctypes. Output includes host timestamps and drop counters.
"""
import argparse
import ctypes as c
import ctypes.util
import sys
import time
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--seconds', type=float, default=60)
ap.add_argument('--output', help='Host log file (defaults to stdout)')
ap.add_argument('--vid', type=lambda s: int(s, 0), default=0x2833)
ap.add_argument('--pid', type=lambda s: int(s, 0), default=0x5013)
a = ap.parse_args()
if a.output:
sys.stdout = open(a.output, 'w', buffering=1)
lib = c.CDLL(ctypes.util.find_library('usb-1.0'))
lib.libusb_init.argtypes = [c.POINTER(c.c_void_p)]
lib.libusb_init.restype = c.c_int
lib.libusb_exit.argtypes = [c.c_void_p]
lib.libusb_open_device_with_vid_pid.argtypes = [c.c_void_p, c.c_uint16, c.c_uint16]
lib.libusb_open_device_with_vid_pid.restype = c.c_void_p
lib.libusb_close.argtypes = [c.c_void_p]
lib.libusb_control_transfer.argtypes = [c.c_void_p, c.c_uint8, c.c_uint8,
c.c_uint16, c.c_uint16, c.c_void_p,
c.c_uint16, c.c_uint]
lib.libusb_control_transfer.restype = c.c_int
context = c.c_void_p()
if lib.libusb_init(c.byref(context)):
raise RuntimeError('libusb initialization failed')
handle = None
try:
handle = lib.libusb_open_device_with_vid_pid(context, a.vid, a.pid)
if not handle:
raise RuntimeError('Quest USB device unavailable or permission denied')
buf = c.create_string_buffer(1024)
deadline = time.monotonic() + a.seconds
while time.monotonic() < deadline:
n = lib.libusb_control_transfer(handle, 0xc0, 0x5a, 0x514b,
0x584c, buf, 1024, 1000)
if n < 0:
print(f'{time.time():.6f} USB error {n}', flush=True)
if n == -7: # Timeout is not proof of a disconnect/reset.
continue
return 1
if n < 4:
raise RuntimeError(f'short console header: {n}')
dropped = int.from_bytes(buf.raw[:4], 'little')
if n > 4:
print(f'[{time.time():.6f} dropped={dropped}]', flush=True)
sys.stdout.write(buf.raw[4:n].decode('utf-8', errors='replace'))
sys.stdout.flush()
else:
time.sleep(0.01)
finally:
if handle:
lib.libusb_close(handle)
lib.libusb_exit(context)
return 0
if __name__ == '__main__':
raise SystemExit(main())