Add turnkey Quest Pro alternate Android release tooling

This commit is contained in:
YusufOzmen01 committed 2026-10-04 18:10:07 +03:00
1 parent 4554025189
commit e61edbd7fb
28 files changed
+1117 -29

No files matched your search

+82 -11
View File
@@ -1,9 +1,13 @@
# Quest Pro kexec → Android: Working Setup & Fix Log
Status date: 2026-09-30 (device sessions of 2026-08-26..27 in target uptime)
Current kernel: #243 (Wi-Fi fix; #242 = previous) (`out/captured-low6` capture, SHA-256
`652176285a16cd8e1e611864537e30c9bf2b5a1fe36bdc4bc69223e4c45c21e0`)
Current payload: `quest-pro-kexec/out/ospayload-252fpoll`
Current kernel archive/run: #253 (emitted build counter #250), SHA-256
`5ded7a62459b5cd7d57f79a06e7bb4c6e798df8ead98d4a8c14fe0b187d266a8`.
Current payload: `quest-pro-kexec/out/ospayload-253-ram12-noupdater`.
Latest boot tag: `ram253noupdater`. Full RAM + H.264 hardware codecs validated;
UI confirmed on the preceding full-RAM/Venus boot. This new boot removes CMS
and OTA entrypoints; final visibility awaits user confirmation.
Fresh-map low6 rollback: `out/ospayload-249-audio-noupdater`.
Older payload maps are stale after the system/system_ext image replacements.
> This file documents the complete recipe that got alternate Android to
> **visually render**, tracking active, and first-time setup usable without
@@ -398,17 +402,78 @@ Note: the other stock dlkms (audio, usb-net, powerstate_mgr, ...) still fail
with the same module_layout error; rebuilt versions of all of them are in
`build/diagnostic-kernel` and can be swapped in the same way if needed.
### Full RAM — validated in rescue only, not yet Android
### CMS and alternate OS updater — persistently disabled
`com.oculus.os.cm` was disabled-user but still crash-looped; OSUpdater was
protected from pm disable-user. Both are now removed from the alternate image
scan paths, together with NuxOta, by quarantining their APKs under the image
root `/qkx-disabled/*.disabled` (root-only mode 0600). They are not scanned as
apps. Quarantine also contains original update_engine RC, gold-core RC,
update_engine, update_engine_client, update_verifier, and postinstall binaries.
Neither normal nor gold update-engine service is registered, and native OTA
entrypoints are absent from their original paths. This disables the alternate
Android OS updater mechanisms; it does NOT modify the stock installation or
assert that unrelated firmware-update code has been audited.
Backups: `work/system-pre-disable-updater-cm.img` and
`work/system_ext-pre-disable-updater-cm.img`. Actual system_ext source remains
`work/no-vision-images/system_ext.img` via the no-usb-images symlink.
Script: `tools/disable-alt-updaters.py` (workspace-specific paths; refuses to
overwrite backups). It expands each host image by 256 MiB, unshares ext4
shared blocks before edits, verifies each quarantined file's SHA-256 against
its original, and requires a clean final e2fsck. Restore by replacing host
copies from the backups, reinstalling those two pinned files, and preparing
fresh payload maps; never write installed partitions.
Install verification: system 1452 MiB, system_ext 1518 MiB, both qkx_rawcp
verified. `tools/os-install.sh` now resolves image symlinks before stat/staging:
its former lstat-sized system_ext allocation was wrong. The failed staging
attempt did not write that image; it was deleted/reallocated before retry.
Fresh prep rebuilt both full-RAM and low6 payload maps.
After reboot: sys.boot_completed=1; all three packages absent from pm's installed
package list; no corresponding processes or update-engine services. Explicit
start attempts for update_engine/update_engine_gold failed, and processes
remained absent after a successful 60-frame Venus H.264 encode. Bluetooth ON,
audio card present, proximity and ET/FT fidelity restored. Controllers remain
intentionally deferred. Current final UI visibility awaits user confirmation.
CVP = Qualcomm Computer Vision Processor, a vision accelerator distinct from
Venus codecs. Its firmware NOC (Network-on-Chip interconnect) error remains
open; confirmed tracking/rendering/video successes do not prove CVP is healthy.
### Full RAM — Android UI confirmed; Venus also passes repeated tests
Kernel #253 and `out/ram12-253` register the full 12 GiB physical RAM map.
Two fresh boots with GPU/display kernel probing enabled passed the corrected
read scan and 11,008 MiB simultaneous allocation/pattern tests, zero errors.
The strengthened fill-all-before-verify alias test also passed. Firmware,
secure VM and kernel reservations leave about 11.25 GiB usable.
Alternate Android was deliberately never launched for these RAM attempts;
normal Android payloads still use the conservative low6 map. See
`KEXEC_RAM_12GB_RESCUE.md` for the splash-reservation guard, scanner fixes,
exact image hash, reproduction and remaining caveats.
Initial RAM validation was rescue-only. Subsequent Android test:
`out/ospayload-253-ram12-novidc`, tag `android253ram12nv`, alive at 60 s,
sys.boot_completed=1, MemTotal=11,786,344 KiB (~11.24 GiB). Bluetooth ON,
audio card present, proximity broadcast and ET/FT fidelity request restored;
user_calibrated=true/both_temporal verified. No GPU timeout/soft-lockup messages
in the subsequent check. Visual rendering awaits user confirmation.
Those initial tests were followed by another failed reboot and a confirmed
low6 rollback. The missing handoff coverage was then addressed: the read-only
stock `ion_secmap` snapshot now also enumerates master-side secure SMMU tables
and their secure pools (GPU, display, CVP, video, crypto and FastRPC domains).
These pages retain HLOS RW, but not EXEC, after stock shares them with a secure
VM; the previous ION-only snapshot omitted them. Reserve them across kexec,
never change their ownership. Initial snapshot: 30 table pages in 9 domains.
With the expanded reservation, full-RAM Android UI was confirmed by the user
(`ram253sharedtables`). A live 4096 MiB allocator/fill-all/verify test passed
with zero mismatches; Bluetooth ON, audio card present and calibration intact.
VIDC was then re-enabled with the established reload-first/no-PC flags:
`ram253sharedvenus` and fresh `ram253sharedvenusrepeat` both completed Android
boot and three 1920x1080, 300-frame H.264 encode/decode cycles apiece, EOS=true.
Host FFmpeg decoded the captured output without errors. Latest MemTotal:
11,774,064 KiB. UI visibility on the final Venus-enabled boot awaits user
confirmation. The unsuccessful low-page-table-allocation diagnostic was
removed; it is not needed by the working configuration.
The previously verified low6 audio/Venus payload remains available. See
`KEXEC_RAM_12GB_RESCUE.md` for the full history and safeguards.
### Audio — audible output confirmed by user on kernel #249
@@ -441,9 +506,15 @@ no functional audio-driver patch was necessary. Installed partitions and
stock userdata were not modified. Gaze fidelity and proximity activation
were restored after this boot. Changes remain uncommitted.
### Venus H.264 encode/decode — VERIFIED on kernel #249
### Venus H.264 encode/decode — verified on low6 #249 and full-RAM run #253
Current Venus payload: `out/ospayload-249-venus`.
Current combined payload: `out/ospayload-253-ram12-venus` (full RAM,
expanded stock secure-table reservation, same Venus reload-first/no-PC flags).
Six 1080p/300-frame encode/decode cycles passed across two fresh boots.
Independent host output: `work/venus-test/encoded-ram12-253.mp4` (FFmpeg exit 0).
Detached completion-driven test: `tools/tests/venus/ram12-check.sh`;
wait for an actual `STATUS=PASS` line, not an echoed shell command.
The historical low6 test payload was `out/ospayload-249-venus`.
Kernel: `build/runs/249/Image`, SHA-256
`a5b35a9a8fa6584e629fe36a77cda526e4d7ff7bfd8baec59a88b52703cfb5a0`.
Build and launch from `quest-pro-kexec` using:
+68 -3
View File
@@ -10,7 +10,15 @@ and recovery environment. No installed partitions or userdata images changed.
Image: `build/runs/253/Image`
SHA-256: `5ded7a62459b5cd7d57f79a06e7bb4c6e798df8ead98d4a8c14fe0b187d266a8`.
Current target: rescue boot `ram12-253repeat`, not Android.
Current target: full-RAM Android with Venus and CMS/updater disabled,
`out/ospayload-253-ram12-noupdater`, tag `ram253noupdater`.
User confirmed visible UI on the earlier combined Venus/full-RAM boot.
The newest CMS/updater-removal boot completed initialization and a Venus
encode test; final visibility awaits confirmation. Image replacements require
fresh maps: prior payload maps are now stale. Fresh low6 rollback:
`out/ospayload-249-audio-noupdater`. See the working-setup document.
The archive/run identifier is 253; this Image's emitted kernel build counter
is #250.
The physical RAM population is 12 GiB, with normal firmware/address-map holes,
secure VM reservations, kernel metadata and splash reservations unavailable to
@@ -37,8 +45,65 @@ memory must remain excluded. Dynamic secure reservations vary between boots.
This is substantial allocation/read/write validation, not exhaustive testing
of all bit patterns or long-duration thermal/retention behavior. Android with
this full map has deliberately NOT been tested. Existing Android payloads and
image maps were left alone; do not call the Android RAM issue resolved yet.
this full map has deliberately NOT been tested. Existing baseline Android payloads and image maps were left alone during
rescue validation. Subsequent Android tests are recorded below; Venus/full-RAM
coexistence is not yet verified.
## Subsequent Android test (user authorized Android after rescue validation)
- Venus-enabled `out/ospayload-253-ram12`, tag `android253ram12`: gadget gone
at 40 s, no completed Android boot established. Retained log shows CPU0
soft lockup waiting in cross-CPU synchronization during BPF setup, with
another CPU not responding. This is not proof of a DDR fault or a proven
Venus root cause.
- `out/ospayload-253-ram12-novidc`, tag `android253ram12nv`: same full memory
map and splash guard, VIDC disabled, alive at 60 s, sys.boot_completed=1.
MemTotal 11,786,344 KiB (~11.24 GiB), MemAvailable 9,050,508 KiB at initial
check. Bluetooth ON, ALSA audio card registered. Proximity and ET/FT restored,
user calibration true/both_temporal verified. No GPU timeout or soft-lockup
messages in the subsequent check. Visual rendering awaits user confirmation.
- No image replacements or installed partition changes for these tests.
## Full-RAM Android success and restored Venus
Initial full-RAM Android rendering was blank and the next reboot soft-locked,
even with VIDC disabled; low6 rollback restored visible UI. The expanded
read-only stock snapshot in `module/ion_secmap.c` + `module/smmu_secmap.h`
then added master-side secure SMMU page tables and cached secure table pools.
Their private stock layouts are corroborated through public VMID/TTBR attrs,
and table traversal is serialized with the driver's assignment/table locks.
Only table pages/pools are read; mapped secure payload pages are not read.
The stock driver shares these pages with HLOS READ|WRITE but no EXEC. Reusing
them as ordinary executable/JIT RAM after kexec is unsafe, despite passing
read/write-only tests. The former ION/KGSL-buffer snapshot omitted these table
pages. The first expanded snapshot found 30 table pages across nine domains:
GPU, crypto, CVP, display, video and FastRPC. Current loader reserves their
ranges along with the existing secure ION ranges; no giveback/SCM reassignment.
This omission is strongly implicated in the Android-specific stalls; not every
historical CPU/GPU fault has been individually attributed.
- `ram253sharedtables` (VIDC disabled): Android boot complete, user confirmed
visible UI. Live Android test allocated 4096 MiB, filled all blocks before
verifying, zero mismatches, all pages freed. Boot completion, Bluetooth ON,
audio card and stock-imported calibration remained intact afterward.
- `ram253sharedvenus` and fresh `ram253sharedvenusrepeat`: VIDC enabled with
qkx_venus_reload_first=1 and qkx_venus_no_pc=1. Both boots completed Android
initialization and each passed three 1080p H.264 hardware encode/decode
cycles (300 frames each, EOS=true); detached script STATUS=PASS.
- Downloaded output `work/venus-test/encoded-ram12-253.mp4`: host ffprobe
H.264/1920x1080/300 frames/10 seconds; FFmpeg full decode exit 0.
- Latest MemTotal 11,774,064 KiB; no GPU timeout, stalled-SMMU or soft-lockup
messages in the final checks. CVP firmware still reported a separate NOC
error during boot; this is not a Venus test failure and remains open.
- Visibility on the final combined Venus-enabled boot still needs user
confirmation. Long-duration/other-codec/secure-DRM tests remain open.
- The low-IOMMU-table-allocation diagnostic (#254 archive) did not resolve the
stalls, was removed from source, and is not part of this working recipe.
Before reproduction, rebuild/use the expanded stock-compatible ion_secmap
module as `module/ion_secmap.ko`. It is essential to the Android recipe.
Historical rescue tests below preceded that snapshot enhancement.
## Changes needed
+107
View File
@@ -0,0 +1,107 @@
# Turnkey Quest Pro alternate Android package
## Split release format
The recommended release uses a device-independent loader ZIP and a separate
ROM ZIP. Extract the loader ZIP, then run:
```sh
./qkx-install-package.sh /path/to/qkx-quest-pro-rom.zip
./qkx-boot.sh
```
The loader contains the kernel, BusyBox, kernel modules, installer, initramfs
and device helpers. The ROM ZIP contains only the five prepared Android images
and turnkey metadata. Neither contains stock calibration or userdata. Existing
alternate `/data` can be retained during ROM replacement; fresh extent maps are
still generated and validated.
Build them with:
```sh
python3 tools/build-rom-package.py IMAGE_DIR rom.zip
python3 tools/build-loader-package.py loader.zip --kernel /path/to/Image
```
## Files
- `qkx-install-package.sh`: interactive installation and package validation.
- `qkx-boot.sh`: boots an installed package without repeating package validation.
- `work/qkx-quest-pro-turnkey-v1.zip`: generated system package.
The package contains the five alternate Android images, known-good kernel #253,
BusyBox, kexec loader, secure-map collector, retained-log helper, and SHA-256
manifest. It contains no userdata and no eye calibration.
## Install
Requirements: Linux host, rooted Quest Pro connected through ADB, Python 3,
`dtc`, `fdtput`, `adb`, e2fsprogs, and the QKX repository/tool binaries.
```sh
cd quest-pro-kexec
./qkx-install-package.sh /path/to/qkx-quest-pro-turnkey-v1.zip
```
The installer asks for alternate `/data` size (4–128 GiB). It verifies every
ZIP member against `manifest.json`, checks the device is `seacliff`, captures
the device-specific runtime DT, and installs images only into pinned files under
`/data/local/tmp/qkx-release`. It reads `modem_a` into another pinned copy; the
partition itself is never written. Existing installed partitions are never
flashed or formatted.
Revalidate a package without installing:
```sh
./qkx-install-package.sh --verify-only /path/to/package.zip
```
## Boot
```sh
./qkx-boot.sh
```
Boot intentionally does not revalidate the ZIP. It uses the verified installed
state, reads the current stock eye calibration into a private temporary
initramfs, snapshots current secure mappings, and performs kexec. The initramfs
mounts the pinned images and launches Android automatically.
On pristine alternate `/data`, the image synchronously creates the vision data
directory, bypasses NUX/provisioning, enables hand tracking and gaze hover,
imports current stock eye calibration, requests high ET/FT fidelity, starts the
Home activity, and leaves CMS, OTA/update engine, DeviceCert/QSEE/SPSS paths
unavailable. Stock calibration and stock userdata are read-only.
## Uninstall from the headset
Reboot into rooted stock Android first, then run:
```sh
./qkx-uninstall.sh
```
The script lists exact QKX-owned userdata paths and requires typing `REMOVE`.
It deletes the current pinned installation, legacy QKX installations, and known
staging files. It refuses to operate from a QKX kernel or while a helper module
is loaded. It never writes, formats, or flashes a partition.
Automation and legacy-retention options:
```sh
./qkx-uninstall.sh --yes
./qkx-uninstall.sh --keep-legacy
```
Host-side packages and `state/` are intentionally retained.
## Build a package
```sh
python3 tools/build-system-package.py \
../work/turnkey-images-v4 ../work/qkx-quest-pro-turnkey-v1.zip \
--kernel ../build/runs/253/Image --repo .
```
Always distribute the ZIP and its separately recorded SHA-256. Do not add image
files, generated state, or calibration material to Git.
+4
View File
@@ -81,6 +81,10 @@ def main():
raise SystemExit(f"{partitions}: missing; rerun tools/os-install.sh")
entries.append(("etc/qkx/partitions", stat.S_IFREG | 0o644,
partitions.read_bytes(), 0, 0))
turnkey = a.os_dir / "qkx-turnkey.json"
if turnkey.is_file():
entries.append(("etc/qkx/turnkey.json", stat.S_IFREG | 0o600,
turnkey.read_bytes(), 0, 0))
maps = sorted((a.os_dir / "maps").glob("*.map"))
if not maps:
raise SystemExit(f"{a.os_dir}/maps: no extent maps found")
+4 -1
View File
@@ -88,7 +88,10 @@ if [ -d /etc/qkx/maps ]; then
qkx-mount-os >/tmp/mount-os.log 2>&1
status=$?
cat /tmp/mount-os.log
if [ $status -eq 0 ]; then
if [ $status -eq 0 ] && [ -f /etc/qkx/turnkey.json ]; then
log 'turnkey OS mounted; launching Android automatically'
qkx-launch-android
elif [ $status -eq 0 ]; then
log 'custom OS mounted at /android; run qkx-launch-android to start it'
else
log "qkx-mount-os failed (status $status); see /tmp/mount-os.log"
+7
View File
@@ -122,6 +122,13 @@ for n in /dev/qkx/*; do
mknod "$ROOT$n" b $((0x$1)) $((0x$2))
done
# Private per-launch calibration arrives from the running stock OS, not /data.
# Keep it on the new /dev tmpfs so it cannot become part of a distributable image.
if [ -s /qkx-stock-eye-calibration.json ]; then
cp /qkx-stock-eye-calibration.json "$ROOT/dev/qkx-stock-eye-calibration.json"
chmod 600 "$ROOT/dev/qkx-stock-eye-calibration.json"
fi
qkx-patch-fstab "$ROOT"
qkx-patch-usb "$ROOT"
qkx-apply-overrides "$ROOT"
+3
View File
@@ -91,6 +91,8 @@ static unsigned long sg_total(struct sg_table *sgt)
return n;
}
#include "smmu_secmap.h"
/* The GPU driver locks its secure buffers to VMID_CP_PIXEL on its own. */
static void dump_kgsl(void)
{
@@ -177,6 +179,7 @@ static int __init ion_secmap_init(void)
up_read(&idev->lock);
dump_kgsl();
dump_smmu_tables();
emit(0, 0);
pr_info("ionsec: total %lu KiB\n", total >> 10);
filp_close(f, NULL);
+104
View File
@@ -0,0 +1,104 @@
/* Read-only snapshot of stock master-side secure SMMU table pages.
* Layout copied from the matching stock arm-smmu.c; public VMID/TTBR attrs
* corroborate it before traversal. HLOS has RW but not EXEC on these pages. */
#include <linux/iommu.h>
#include <linux/io-pgtable.h>
#include <linux/platform_device.h>
#include "kgsl_iommu.h"
struct secmap_smmu_cfg {
u8 cbndx, irptndx; u16 asid; u32 cbar, procid; int fmt;
u16 min_asid, max_asid;
};
struct secmap_smmu_domain {
void *smmu; struct device *dev; struct io_pgtable_ops *pgtbl_ops[2];
const struct iommu_gather_ops *tlb_ops; struct secmap_smmu_cfg cfg;
int stage; struct mutex init_mutex; spinlock_t cb_lock, sync_lock;
struct io_pgtable_cfg pgtbl_cfg[2]; u32 attributes;
bool slave_side_secure; u32 secure_vmid;
struct list_head pte_info_list, unassign_list; struct mutex assign_lock;
struct list_head secure_pool_list, nonsecure_pool; void *logger;
struct iommu_domain domain;
};
struct secmap_pool_chunk { void *virt; size_t size; struct list_head list; };
static unsigned long smmu_tables;
static struct iommu_domain *seen_domains[256];
static unsigned int seen_count;
static bool secmap_ram(phys_addr_t pa)
{
return pa >= 0x80000000ULL && pa < 0x380000000ULL && pfn_valid(PHYS_PFN(pa));
}
static void secmap_walk(phys_addr_t pa, unsigned int level, unsigned int entries)
{
u64 *pte;
unsigned int i;
if (!secmap_ram(pa) || level > 3 || entries > 512) return;
emit(pa & PAGE_MASK, PAGE_SIZE);
smmu_tables++;
if (level == 3) return;
pte = phys_to_virt(pa);
for (i = 0; i < entries; i++) {
u64 x = READ_ONCE(pte[i]);
if ((x & 3) == 3)
secmap_walk(x & GENMASK_ULL(47, 12), level + 1, 512);
}
}
static void secmap_domain(struct iommu_domain *domain, const char *name)
{
struct secmap_smmu_domain *d;
struct secmap_pool_chunk *chunk;
int vmid, ret;
u64 ttbr;
unsigned int i, levels, entries;
unsigned long flags;
if (!domain || seen_count == ARRAY_SIZE(seen_domains)) return;
for (i = 0; i < seen_count; i++) if (seen_domains[i] == domain) return;
ret = iommu_domain_get_attr(domain, DOMAIN_ATTR_SECURE_VMID, &vmid);
if (ret || vmid <= 0 || vmid >= 64) return;
ret = iommu_domain_get_attr(domain, DOMAIN_ATTR_TTBR0, &ttbr);
if (ret || !ttbr) return;
d = container_of(domain, struct secmap_smmu_domain, domain);
if (d->secure_vmid != vmid || d->slave_side_secure || d->stage != 0 ||
(ttbr & GENMASK_ULL(47, 12)) !=
(d->pgtbl_cfg[0].arm_lpae_s1_cfg.ttbr[0] & GENMASK_ULL(47, 12))) {
pr_info("ionsec: smmu layout/type rejected %s vmid=%d\n", name, vmid);
return;
}
seen_domains[seen_count++] = domain;
pr_info("ionsec: smmu %s vmid=%d ias=%u oas=%u\n", name, vmid,
d->pgtbl_cfg[0].ias, d->pgtbl_cfg[0].oas);
mutex_lock(&d->assign_lock);
spin_lock_irqsave(&d->cb_lock, flags);
for (i = 0; i < 2; i++) {
struct io_pgtable_cfg *c = &d->pgtbl_cfg[i];
if (!d->pgtbl_ops[i] || c->ias < 25 || c->ias > 48 ||
!(c->pgsize_bitmap & PAGE_SIZE)) continue;
levels = DIV_ROUND_UP(c->ias - 12, 9);
entries = 1U << (c->ias - 12 - 9 * (levels - 1));
if (c->arm_lpae_s1_cfg.ttbr[0])
secmap_walk(c->arm_lpae_s1_cfg.ttbr[0] & GENMASK_ULL(47, 12), 4 - levels, entries);
if (c->arm_lpae_s1_cfg.ttbr[1])
secmap_walk(c->arm_lpae_s1_cfg.ttbr[1] & GENMASK_ULL(47, 12), 4 - levels, entries);
}
list_for_each_entry(chunk, &d->secure_pool_list, list)
if (secmap_ram(virt_to_phys(chunk->virt))) emit(virt_to_phys(chunk->virt), chunk->size);
spin_unlock_irqrestore(&d->cb_lock, flags);
mutex_unlock(&d->assign_lock);
}
static int secmap_device(struct device *dev, void *unused)
{
secmap_domain(iommu_get_domain_for_dev(dev), dev_name(dev));
return 0;
}
static void dump_smmu_tables(void)
{
if (kgsl_drv && kgsl_drv->devp[0]) {
struct kgsl_pagetable *pt = kgsl_drv->devp[0]->mmu.securepagetable;
if (pt && pt->priv) {
struct kgsl_iommu_pt *ipt = pt->priv;
secmap_domain(ipt->domain, "kgsl-secure");
}
}
bus_for_each_dev(&platform_bus_type, NULL, NULL, secmap_device);
pr_info("ionsec: smmu table pages=%lu domains=%u\n", smmu_tables, seen_count);
}
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Boot the already-installed QKX OS. Deliberately does not revalidate package files.
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
adb shell 'su -c id' | grep -q 'uid=0' || { echo 'Rooted stock Android adb is required'; exit 1; }
SERIAL=$(adb get-serialno | tr -cd 'A-Za-z0-9_.-')
STATE="$HERE/state/$SERIAL"
if [ ! -d "$STATE" ] && [ -f "$HERE/state/current" ]; then STATE="$HERE/state/$(cat "$HERE/state/current")"; fi
for f in "$STATE/payload/Image" "$STATE/payload/initramfs" "$STATE/payload/boot.dtb" \
"$STATE/modules/quest_kexec.ko" "$STATE/modules/ion_secmap.ko" "$STATE/modules/marker_read.ko"; do
[ -f "$f" ] || { echo "Not installed: $f"; exit 1; }
done
export QKX_LOADER_MODULE="$STATE/modules/quest_kexec.ko"
export QKX_SECMAP_MODULE="$STATE/modules/ion_secmap.ko"
export QKX_MARKER_MODULE="$STATE/modules/marker_read.ko"
export QKX_STOP_QSEE=1
# run.sh captures current stock calibration into a private temporary initramfs,
# snapshots secure mappings, and performs the jump. Turnkey initramfs launches
# Android automatically; there is intentionally no rescue-shell launch step.
"$HERE/tools/run.sh" "$STATE/payload" shutdown_adsp=1 shutdown_cdsp=1 \
shutdown_subsys=venus,npu phase_delay_ms=0
echo 'QKX jump initiated; Android will start automatically.'
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Install a verified QKX system package into pinned userdata files.
# Installed partitions are never written. Usage: ./qkx-install-package.sh [ZIP]
# Revalidation only: ./qkx-install-package.sh --verify-only ZIP
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
VERIFY_ONLY=0
if [ "${1:-}" = --verify-only ]; then VERIFY_ONLY=1; shift; fi
ZIP=${1:-}
if [ -z "$ZIP" ]; then read -r -p 'System package ZIP: ' ZIP; fi
ZIP=$(readlink -f "$ZIP")
[ -f "$ZIP" ] || { echo "Package not found: $ZIP"; exit 1; }
verify_zip() {
python3 - "$1" <<'PY'
import hashlib,json,sys,zipfile
z=zipfile.ZipFile(sys.argv[1]); names=set(z.namelist())
try: m=json.loads(z.read('manifest.json'))
except Exception as e: raise SystemExit('invalid manifest: '+str(e))
fmt=m.get('format')
if fmt not in ('qkx-system-package-v1','qkx-rom-package-v1') or m.get('device')!='Quest Pro (seacliff)':
raise SystemExit('unsupported package/device')
required={'images/'+x+'.img' for x in ('system','system_ext','vendor','odm','product')}
required|={'qkx-turnkey.json'}
if fmt=='qkx-system-package-v1':
required|={'kernel/Image','runtime/busybox','modules/quest_kexec.ko','modules/ion_secmap.ko','modules/marker_read.ko'}
if not required <= set(m.get('files',{})): raise SystemExit('manifest is incomplete')
for n,s in m['files'].items():
if n not in names or n.startswith('/') or '..' in n.split('/'): raise SystemExit('unsafe/missing member: '+n)
h=hashlib.sha256(); size=0
with z.open(n) as f:
for b in iter(lambda:f.read(8<<20),b''): h.update(b); size+=len(b)
if size!=s['size'] or h.hexdigest()!=s['sha256']: raise SystemExit('HASH FAILURE: '+n)
print('VERIFIED',n)
print('PACKAGE VERIFIED')
PY
}
verify_zip "$ZIP"
[ "$VERIFY_ONLY" = 0 ] || exit 0
adb shell 'su -c id' </dev/null | grep -q 'uid=0' || { echo 'Rooted adb is required'; exit 1; }
MODEL=$(adb shell getprop ro.product.device </dev/null | tr -d '\r')
[ "$MODEL" = seacliff ] || { echo "Refusing non-Quest-Pro device: $MODEL"; exit 1; }
read -r -p 'Alternate /data size in GiB [16]: ' DATA_GIB
DATA_GIB=${DATA_GIB:-16}
[[ "$DATA_GIB" =~ ^[0-9]+$ ]] && [ "$DATA_GIB" -ge 4 ] && [ "$DATA_GIB" -le 128 ] || { echo 'Choose 4..128 GiB'; exit 1; }
SERIAL=$(adb get-serialno | tr -cd 'A-Za-z0-9_.-')
STATE="$HERE/state/$SERIAL"; WORK=/data/local/tmp/qkx-release
if adb exec-out "su -c 'test -e $WORK/img/system.img && echo YES'" </dev/null | grep -q YES; then
read -r -p 'Replace the existing QKX release installation? [y/N] ' ans
[[ "$ans" = y || "$ans" = Y ]] || exit 1
read -r -p 'Preserve the existing alternate /data image? [Y/n] ' keep_data
if [[ "$keep_data" = n || "$keep_data" = N ]]; then
adb shell "su -c 'rm -rf $WORK'" </dev/null
else
# os-install.sh detects the retained data.img, skips rebuilding/transferring
# it, and emits a fresh extent map along with the replaced OS image maps.
adb shell "su -c 'rm -f $WORK/img/system.img $WORK/img/system_ext.img \
$WORK/img/vendor.img $WORK/img/odm.img $WORK/img/product.img \
$WORK/stage.img $WORK/stage.img.gz'" </dev/null
fi
fi
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
python3 - "$ZIP" "$TMP" <<'PY'
import sys,zipfile
with zipfile.ZipFile(sys.argv[1]) as z: z.extractall(sys.argv[2])
PY
# Split ROM packages obtain the device-independent loader binaries from this
# release bundle. Combined legacy packages continue to carry their own copies.
if [ ! -f "$TMP/kernel/Image" ]; then
for f in runtime/Image runtime/busybox runtime/quest_kexec.ko runtime/ion_secmap.ko runtime/marker_read.ko; do
[ -f "$HERE/$f" ] || { echo "Loader package is missing $f"; exit 1; }
done
mkdir -p "$TMP/kernel" "$TMP/runtime" "$TMP/modules"
cp "$HERE/runtime/Image" "$TMP/kernel/Image"
cp "$HERE/runtime/busybox" "$TMP/runtime/busybox"
cp "$HERE/runtime/quest_kexec.ko" "$TMP/modules/quest_kexec.ko"
cp "$HERE/runtime/ion_secmap.ko" "$TMP/modules/ion_secmap.ko"
cp "$HERE/runtime/marker_read.ko" "$TMP/modules/marker_read.ko"
fi
rm -rf "$STATE"; mkdir -p "$STATE/modules" "$STATE/captured" "$STATE/maps"
cp "$TMP"/modules/*.ko "$STATE/modules/"
cp "$TMP/kernel/Image" "$STATE/Image"
cp "$TMP/manifest.json" "$TMP/qkx-turnkey.json" "$STATE/"
"$HERE/tools/capture.sh" "$STATE/captured"
QKX_WORK_DIR="$WORK" "$HERE/tools/os-install.sh" "$TMP/images" "$STATE/maps" "$DATA_GIB"
QKX_WORK_DIR="$WORK" "$HERE/tools/os-install-modemfw.sh" "$STATE/maps"
# Preserve the turnkey marker so initramfs automatically launches Android.
cp "$TMP/qkx-turnkey.json" "$STATE/maps/qkx-turnkey.json"
export BUSYBOX="$TMP/runtime/busybox"
export QKX_EXTRA_CMDLINE='qkx_uart_dma_cleanup=1 qkx_skip_regulator_cleanup=1 deferred_probe_timeout=-1 qkx_stallmon=1 qkx_beat=1 qkx_venus_reload_first=1 qkx_venus_no_pc=1'
"$HERE/tools/os-prep.sh" "$STATE/captured" "$STATE/Image" "$STATE/maps" "$STATE/payload"
fdtput -t s "$STATE/payload/boot.dtb" /soc/qseecom@82400000 status disabled
fdtput -t s "$STATE/payload/boot.dtb" /reserved-memory/cont_splash_region@9c000000 no-map ''
# Runtime DT state can expose the secure-display CMA pool as reusable. The
# verified Android handoff must preserve it, not let Linux allocate from it.
fdtput -d "$STATE/payload/boot.dtb" /reserved-memory/secure_display_region reusable 2>/dev/null || true
fdtput -t s "$STATE/payload/boot.dtb" /reserved-memory/secure_display_region no-map ''
touch "$STATE/payload/require-stock-calibration"
printf '%s\n' "$WORK" > "$STATE/device-work-dir"
printf '%s\n' "$SERIAL" > "$HERE/state/current"
echo "INSTALL VERIFIED AND READY: $STATE"
echo "Boot with: $HERE/qkx-boot.sh"
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Remove QKX-created files from the headset's userdata. Never touches partitions.
# Usage: ./qkx-uninstall.sh [--yes] [--keep-legacy]
set -euo pipefail
YES=0; LEGACY=1
while [ $# -gt 0 ]; do
case "$1" in
--yes|-y) YES=1 ;;
--keep-legacy) LEGACY=0 ;;
--help|-h)
echo "usage: $0 [--yes] [--keep-legacy]"
echo ' --yes do not prompt'
echo ' --keep-legacy retain old /data/local/tmp/qkx and qkx-turnkey-v1 installs'
exit 0 ;;
*) echo "unknown argument: $1"; exit 2 ;;
esac
shift
done
adb shell 'su -c id' 2>/dev/null | grep -q 'uid=0' || {
echo 'Rooted stock Android adb is required.'; exit 1; }
KERNEL=$(adb shell uname -r | tr -d '\r')
case "$KERNEL" in
*qkx*) echo "Refusing to uninstall while a QKX kernel is running ($KERNEL). Reboot stock Android first."; exit 1 ;;
esac
MODEL=$(adb shell getprop ro.product.device | tr -d '\r')
[ "$MODEL" = seacliff ] || { echo "Refusing unexpected device: $MODEL"; exit 1; }
# Exact project-owned paths only. Do not use a broad qkx* glob.
PATHS=(
/data/local/tmp/qkx-release
/data/local/tmp/qkx.ko
/data/local/tmp/qkx_marker.ko
/data/local/tmp/ion_secmap.ko
/data/local/tmp/qkx_giveback.ko
/data/local/tmp/qkx-Image
/data/local/tmp/qkx-initramfs
/data/local/tmp/qkx-boot.dtb
/data/local/tmp/qkx-install.sh
/data/local/tmp/qkx_fsmap
/data/local/tmp/qkx_rawcp
/data/local/tmp/qkx_dm
/data/local/tmp/qkx-inspect-data.map
/data/local/tmp/cvp-alt-firmware-sha.txt
)
if [ "$LEGACY" = 1 ]; then
PATHS+=(/data/local/tmp/qkx /data/local/tmp/qkx-turnkey-v1)
fi
printf -v REMOTE_LIST " '%q'" "${PATHS[@]}"
echo 'QKX-owned headset paths found:'
FOUND=$(adb shell "su -c 'for p in$REMOTE_LIST; do if [ -e \"\$p\" ]; then du -sh \"\$p\" 2>/dev/null || ls -ld \"\$p\"; fi; done'" | tr -d '\r')
if [ -z "$FOUND" ]; then
echo ' (none; headset is already clean)'
exit 0
fi
printf '%s\n' "$FOUND" | sed 's/^/ /'
echo
echo 'This only deletes files on userdata; no installed partition is flashed, formatted, or modified.'
if [ "$YES" != 1 ]; then
read -r -p 'Delete these QKX files? Type REMOVE to continue: ' answer
[ "$answer" = REMOVE ] || { echo 'Cancelled.'; exit 1; }
fi
# A loaded loader should not normally exist after a successful or failed run.
# Refuse rather than changing live kernel/module state in an uninstaller.
LOADED=$(adb shell "su -c 'grep -E \"^(quest_kexec|marker_read|ion_secmap) \" /proc/modules || true'" | tr -d '\r')
if [ -n "$LOADED" ]; then
echo 'A QKX helper module is still loaded; reboot stock Android and rerun:'
printf '%s\n' "$LOADED"
exit 1
fi
adb shell "su -c 'set -e; for p in$REMOTE_LIST; do rm -rf -- \"\$p\"; done; sync; for p in$REMOTE_LIST; do [ ! -e \"\$p\" ] || { echo FAILED:\$p; exit 1; }; done; echo QKX_HEADSET_FILES_REMOVED'" | tr -d '\r' | grep -q '^QKX_HEADSET_FILES_REMOVED$'
echo 'QKX headset files removed successfully.'
echo 'Host-side state and package ZIPs were intentionally retained.'
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Build a standalone Quest Pro QKX loader bundle (no ROM or private data)."""
import argparse, hashlib, os, stat, zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TOP = ('qkx-install-package.sh', 'qkx-boot.sh', 'qkx-uninstall.sh',
'README.md', 'docs/TURNKEY_PACKAGE.md')
TOOLS = ('capture.sh', 'os-install.sh', 'os-install-modemfw.sh', 'os-prep.sh',
'prep.sh', 'prepare.py', 'run.sh', 'stock-calibration-initramfs.py')
def digest(path):
h = hashlib.sha256()
with path.open('rb') as f:
for b in iter(lambda: f.read(8 << 20), b''):
h.update(b)
return h.hexdigest()
def add_tree(items, directory, prefix):
for p in sorted(directory.rglob('*')):
if p.is_file() and '__pycache__' not in p.parts and not p.name.endswith(('.o','.ko','.cmd','.mod','.mod.c')):
items[prefix + '/' + p.relative_to(directory).as_posix()] = p
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('output', type=Path)
ap.add_argument('--kernel', type=Path, required=True)
ap.add_argument('--busybox', type=Path, default=ROOT/'out/busybox')
a = ap.parse_args()
items = {name: ROOT/name for name in TOP}
items.update({'tools/'+name: ROOT/'tools'/name for name in TOOLS})
add_tree(items, ROOT/'initramfs', 'initramfs')
add_tree(items, ROOT/'tools/device', 'tools/device')
items.update({
'runtime/Image': a.kernel.resolve(),
'runtime/busybox': a.busybox.resolve(),
'runtime/quest_kexec.ko': ROOT/'module/quest_kexec.ko',
'runtime/ion_secmap.ko': ROOT/'module/ion_secmap.ko',
'runtime/marker_read.ko': ROOT/'module/marker_read.ko',
})
for arc, path in items.items():
if not path.is_file(): raise SystemExit(f'missing {arc}: {path}')
a.output.parent.mkdir(parents=True, exist_ok=True)
tmp = a.output.with_suffix(a.output.suffix+'.tmp'); tmp.unlink(missing_ok=True)
with zipfile.ZipFile(tmp, 'w', allowZip64=True) as z:
for arc, path in sorted(items.items()):
info = zipfile.ZipInfo('qkx-quest-pro-loader/'+arc)
info.external_attr = ((path.stat().st_mode & 0xffff) << 16)
info.compress_type = zipfile.ZIP_DEFLATED
z.writestr(info, path.read_bytes(), compresslevel=6)
os.replace(tmp, a.output)
print(a.output, digest(a.output))
if __name__ == '__main__': main()
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Build a ROM-only ZIP consumed by the standalone QKX loader package."""
import argparse, hashlib, json, os, zipfile
from pathlib import Path
FILES = {f'images/{n}.img': f'{n}.img' for n in
('system', 'system_ext', 'vendor', 'odm', 'product')}
def digest(path):
h = hashlib.sha256()
with path.open('rb') as f:
for block in iter(lambda: f.read(8 << 20), b''):
h.update(block)
return h.hexdigest()
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('images', type=Path)
p.add_argument('output', type=Path)
a = p.parse_args()
sources = {arc: (a.images / name).resolve() for arc, name in FILES.items()}
sources['qkx-turnkey.json'] = (a.images / 'qkx-turnkey.json').resolve()
for arc, path in sources.items():
if not path.is_file():
raise SystemExit(f'missing {arc}: {path}')
manifest = {'format': 'qkx-rom-package-v1', 'device': 'Quest Pro (seacliff)',
'calibration': 'read-current-stock-at-each-boot', 'files': {}}
for arc, path in sources.items():
manifest['files'][arc] = {'size': path.stat().st_size,
'sha256': digest(path)}
print('HASHED', arc)
a.output.parent.mkdir(parents=True, exist_ok=True)
tmp = a.output.with_suffix(a.output.suffix + '.tmp')
tmp.unlink(missing_ok=True)
with zipfile.ZipFile(tmp, 'w', allowZip64=True) as z:
z.writestr('manifest.json', json.dumps(manifest, indent=2) + '\n',
compress_type=zipfile.ZIP_DEFLATED)
for arc, path in sources.items():
z.write(path, arc, compress_type=zipfile.ZIP_DEFLATED, compresslevel=3)
os.replace(tmp, a.output)
print(a.output, digest(a.output))
if __name__ == '__main__':
main()
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Build the distributable Quest Pro alternate-Android ZIP and hash manifest."""
import argparse, hashlib, json, os, tempfile, zipfile
from pathlib import Path
FILES = {
'images/system.img': 'system.img', 'images/system_ext.img': 'system_ext.img',
'images/vendor.img': 'vendor.img', 'images/odm.img': 'odm.img',
'images/product.img': 'product.img',
}
def digest(path):
h=hashlib.sha256()
with path.open('rb') as f:
for b in iter(lambda:f.read(8<<20), b''): h.update(b)
return h.hexdigest()
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('images',type=Path); p.add_argument('output',type=Path)
p.add_argument('--kernel',type=Path,required=True); p.add_argument('--repo',type=Path,default=Path(__file__).resolve().parents[1])
a=p.parse_args(); repo=a.repo.resolve()
sources={arc:(a.images/src).resolve() for arc,src in FILES.items()}
sources.update({'kernel/Image':a.kernel.resolve(),
'runtime/busybox':(repo/'out/busybox').resolve(),
'modules/quest_kexec.ko':repo/'module/quest_kexec.ko',
'modules/ion_secmap.ko':repo/'module/ion_secmap.ko',
'modules/marker_read.ko':repo/'module/marker_read.ko'})
for arc,path in sources.items():
if not path.is_file(): raise SystemExit(f'missing {arc}: {path}')
turnkey=a.images/'qkx-turnkey.json'
if not turnkey.is_file(): raise SystemExit('images are not a turnkey set')
sources['qkx-turnkey.json']=turnkey
manifest={'format':'qkx-system-package-v1','device':'Quest Pro (seacliff)',
'calibration':'read-current-stock-at-each-boot','files':{}}
for arc,path in sources.items():
manifest['files'][arc]={'size':path.stat().st_size,'sha256':digest(path)}
print('HASHED',arc)
a.output.parent.mkdir(parents=True,exist_ok=True)
tmp=a.output.with_suffix(a.output.suffix+'.tmp'); tmp.unlink(missing_ok=True)
with zipfile.ZipFile(tmp,'w',allowZip64=True) as z:
z.writestr('manifest.json',json.dumps(manifest,indent=2)+'\n',compress_type=zipfile.ZIP_DEFLATED)
for arc,path in sources.items():
# Android sparse/ext4 images compress well; binaries do not need maximum CPU cost.
z.write(path,arc,compress_type=zipfile.ZIP_DEFLATED,compresslevel=3)
os.replace(tmp,a.output)
print(a.output, digest(a.output))
if __name__=='__main__': main()
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Create alternate-only turnkey images from the known-working image set.
Source images remain untouched. Calibration/user data are never included.
"""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import tempfile
HERE = Path(__file__).resolve().parent
NAMES = ('system', 'system_ext', 'vendor', 'odm', 'product')
REMOVE = {
'system': ['/system/etc/init/update_engine.rc', '/system/bin/update_engine',
'/system/bin/update_engine_client', '/system/bin/update_verifier', '/system/bin/postinstall'],
'system_ext': ['/etc/init/update_engine_gold.rc', '/priv-app/OSUpdater/OSUpdater.apk',
'/priv-app/NuxOta/NuxOta.apk', '/priv-app/CMSHeadset/CMSHeadset.apk',
'/priv-app/FirstTimeNuxSeacliff/FirstTimeNuxSeacliff.apk'],
}
def run(*args):
return subprocess.run(args, check=True, capture_output=True, text=True).stdout
def fsck(image, *opts):
p = subprocess.run(['e2fsck', *opts, str(image)], capture_output=True, text=True)
if p.returncode not in ((0,) if '-n' in opts or '-fn' in opts else (0, 1)):
raise RuntimeError(p.stdout + p.stderr)
def debug(image, command, write=False):
p = subprocess.run(['debugfs', *(['-w'] if write else []), '-R', command, str(image)],
capture_output=True, text=True)
result = p.stdout + p.stderr
if p.returncode or any(x in result.lower() for x in
['could not allocate', 'filesystem full', 'no space']):
raise RuntimeError(result)
return result
def exists(image, path):
return 'File not found' not in debug(image, 'stat ' + path)
def put(image, source, dest, tmp, mode=0o644):
if exists(image, dest):
debug(image, 'rm ' + dest, True)
debug(image, f'write {source} {dest}', True)
debug(image, f'set_inode_field {dest} mode {(0o100000 | mode):07o}', True)
debug(image, f'set_inode_field {dest} uid 0', True)
debug(image, f'set_inode_field {dest} gid 0', True)
debug(image, f'ea_set -f {tmp / "label"} {dest} security.selinux', True)
verify = tmp / 'verify'
verify.unlink(missing_ok=True)
debug(image, f'dump {dest} {verify}')
if verify.read_bytes() != source.read_bytes():
raise RuntimeError('Image byte verification failed: ' + dest)
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('source', type=Path)
ap.add_argument('output', type=Path)
ap.add_argument('--faceeye-request', type=Path, required=True)
a = ap.parse_args()
if a.output.exists():
raise SystemExit('Refusing to overwrite output directory')
for n in NAMES:
if not (a.source / (n + '.img')).resolve().is_file():
raise SystemExit('Missing source image: ' + n)
if not a.faceeye_request.is_file():
raise SystemExit('Build faceeye-request.cpp for Android ARM64 first')
a.output.mkdir(parents=True)
manifest = {'version': 1, 'stock_calibration_required': True,
'source_images': {}, 'quarantined': [], 'images': {}}
with tempfile.TemporaryDirectory(prefix='qkx-turnkey-') as temp:
tmp = Path(temp)
(tmp / 'label').write_bytes(b'u:object_r:system_file:s0\0')
for n in NAMES:
src = (a.source / (n + '.img')).resolve()
image = a.output / (n + '.img')
run('cp', '--reflink=auto', '--sparse=always', str(src), str(image))
manifest['source_images'][n] = str(src)
if n in REMOVE or n == 'product':
with image.open('r+b') as f:
f.truncate(image.stat().st_size + (64 << 20))
fsck(image, '-fy')
run('resize2fs', str(image))
fsck(image, '-fy', '-E', 'unshare_blocks')
if n in REMOVE:
if not exists(image, '/qkx-disabled'):
debug(image, 'mkdir /qkx-disabled', True)
for path in REMOVE[n]:
if not exists(image, path):
continue # Already quarantined by the baseline.
original = tmp / 'original'
original.unlink(missing_ok=True)
debug(image, f'dump {path} {original}')
dest = '/qkx-disabled/' + path.replace('/', '_') + '.disabled'
put(image, original, dest, tmp, 0o600)
debug(image, 'rm ' + path, True)
if exists(image, path):
raise RuntimeError('Quarantine failed: ' + path)
manifest['quarantined'].append(n + ':' + path)
if n == 'system':
put(image, HERE / 'turnkey/qkx-defaults', '/system/bin/qkx-defaults', tmp, 0o755)
put(image, HERE / 'turnkey/qkx-defaults.rc', '/system/etc/init/qkx-defaults.rc', tmp)
put(image, HERE / 'turnkey/qkx-start-keystore', '/system/bin/qkx-start-keystore', tmp, 0o755)
put(image, HERE / 'turnkey/qkx-early-keystore.rc', '/system/etc/init/qkx-early-keystore.rc', tmp)
put(image, a.faceeye_request, '/system/bin/qkx-faceeye-request', tmp, 0o755)
elif n == 'product':
put(image, HERE / 'turnkey/QkxFrameworkOverlay.apk',
'/overlay/QkxFrameworkOverlay.apk', tmp)
fsck(image, '-fn')
manifest['images'][n] = hashlib.file_digest(image.open('rb'), 'sha256').hexdigest()
print('VERIFIED/CLEAN', image)
(a.output / 'qkx-turnkey.json').write_text(json.dumps(manifest, indent=2) + '\n')
if __name__ == '__main__':
main()
+2 -1
View File
@@ -9,7 +9,8 @@
# No partition is formatted and nothing outside these files is written.
set -e
W=/data/local/tmp/qkx
W=$(dirname "$0")
case "$W" in /data/local/tmp/qkx*) ;; *) echo 'invalid pinned-file work directory'; exit 1 ;; esac
BIN=$W/bin
IMG=$W/img
MAP=$W/maps
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Quarantine alternate-image OTA entrypoints and crash-looping CMS APK only.
Never opens a block device. Back up/unshare before editing Android ext4 images.
"""
from pathlib import Path
import hashlib, subprocess, tempfile
ROOT=Path('/home/yusuf/kexectest/work')
IMAGES=ROOT/'no-usb-images'
FILES={
'system': ['/system/etc/init/update_engine.rc', '/system/bin/update_engine',
'/system/bin/update_engine_client', '/system/bin/update_verifier', '/system/bin/postinstall'],
'system_ext': ['/etc/init/update_engine_gold.rc', '/priv-app/OSUpdater/OSUpdater.apk',
'/priv-app/NuxOta/NuxOta.apk', '/priv-app/CMSHeadset/CMSHeadset.apk'],
}
def fsck(img, *args):
p=subprocess.run(['e2fsck',*args,str(img)],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
print(p.stdout.splitlines()[-1])
if p.returncode not in (0,1): raise RuntimeError(p.stdout)
def debug(img, cmd, write=False):
p=subprocess.run(['debugfs',*(['-w'] if write else []),'-R',cmd,str(img)],capture_output=True,text=True)
if p.returncode or any(x in p.stderr.lower() for x in ['could not allocate','filesystem full','no space']):
raise RuntimeError(p.stdout+p.stderr)
return p.stdout+p.stderr
with tempfile.TemporaryDirectory(prefix='qkx-disable-') as tmp:
tmp=Path(tmp)
label=tmp/'label'; label.write_bytes(b'u:object_r:system_file:s0\0')
for name, paths in FILES.items():
img=(IMAGES/f'{name}.img').resolve()
backup=ROOT/f'{name}-pre-disable-updater-cm.img'
if backup.exists(): raise RuntimeError(f'Refusing to overwrite backup {backup}')
subprocess.run(['cp','--reflink=auto',str(img),str(backup)],check=True)
with img.open('r+b') as f: f.truncate(img.stat().st_size+(256<<20))
fsck(img,'-fy')
subprocess.run(['resize2fs',str(img)],check=True)
fsck(img,'-fy','-E','unshare_blocks')
debug(img,'mkdir /qkx-disabled',True)
for path in paths:
src=tmp/Path(path).name
debug(img,f'dump {path} {src}')
if not src.exists() or not src.stat().st_size: raise RuntimeError(f'Missing {path}')
digest=hashlib.sha256(src.read_bytes()).hexdigest()
dest='/qkx-disabled/'+src.name+'.disabled'
debug(img,f'write {src} {dest}',True)
debug(img,f'set_inode_field {dest} mode 0100600',True)
debug(img,f'set_inode_field {dest} uid 0',True)
debug(img,f'set_inode_field {dest} gid 0',True)
debug(img,f'ea_set -f {label} {dest} security.selinux',True)
verify=tmp/'verify'
debug(img,f'dump {dest} {verify}')
assert hashlib.sha256(verify.read_bytes()).hexdigest()==digest, path
debug(img,f'rm {path}',True)
assert 'File not found' in debug(img,f'stat {path}'), path
print('QUARANTINED',name,path,digest)
p=subprocess.run(['e2fsck','-fn',str(img)],capture_output=True,text=True)
if p.returncode: raise RuntimeError(p.stdout+p.stderr)
print('CLEAN',img)
+4 -2
View File
@@ -13,7 +13,7 @@ set -euo pipefail
OUT=${1:-out/os}
HERE=$(cd "$(dirname "$0")/.." && pwd)
W=/data/local/tmp/qkx
W=${QKX_WORK_DIR:-/data/local/tmp/qkx}
NAME=modemfw
say() { printf '\n== %s\n' "$*"; }
@@ -47,7 +47,9 @@ else
sh_ "sh $W/qkx-install.sh write $NAME $W/modemfw-src.img"
say "$NAME: verifying"
sh_ "sh $W/qkx-install.sh verify $NAME $W/modemfw-src.img"
verified=$(sh_ "sh $W/qkx-install.sh verify $NAME $W/modemfw-src.img" 2>&1)
printf '%s\n' "$verified"
grep -q 'qkx_rawcp: verified' <<<"$verified" || { echo 'raw verification failed: modemfw'; exit 1; }
sh_ "rm -f $W/modemfw-src.img"
fi
+20 -5
View File
@@ -16,7 +16,8 @@ OTA=${1:?usage: os-install.sh <ota-image-dir> [out-dir] [data-size-GiB]}
OUT=${2:-out/os}
DATA_GIB=${3:-16}
HERE=$(cd "$(dirname "$0")/.." && pwd)
W=/data/local/tmp/qkx
W=${QKX_WORK_DIR:-/data/local/tmp/qkx}
[[ "$W" =~ ^/data/local/tmp/qkx[-a-zA-Z0-9_]*$ ]] || { echo 'invalid pinned-file work directory'; exit 1; }
# system.img is the root filesystem; the rest mount inside it.
IMAGES=(system system_ext vendor odm product)
@@ -42,7 +43,8 @@ adb push "$HERE/tools/device/qkx_fsmap" "$HERE/tools/device/qkx_rawcp" \
sh_ "mkdir -p $W/bin $W/img $W/maps &&
cp /data/local/tmp/qkx_fsmap /data/local/tmp/qkx_rawcp /data/local/tmp/qkx_dm $W/bin/ &&
cp /data/local/tmp/qkx-install.sh $W/ &&
chmod 755 $W/bin/* $W/qkx-install.sh"
chmod 755 $W/bin/* $W/qkx-install.sh &&
chgrp 2000 $W && chmod 0770 $W"
# Record which block device backs each partition name. The target has no vendor
# init to create by-name symlinks, so its initramfs needs this mapping.
@@ -55,6 +57,9 @@ echo "slot: $(cat "$OUT/slot_suffix")"
install_image() {
local name=$1 src=$2 bytes
# stat otherwise reports the symlink length instead of the image size.
# Resolve it for staging too, so allocation and transfer use one file.
src=$(readlink -f -- "$src")
bytes=$(stat -c %s "$src")
# f2fs allocates in 4 KiB blocks; round up so the whole image fits.
bytes=$(( (bytes + 4095) / 4096 * 4096 ))
@@ -69,7 +74,10 @@ install_image() {
sh_ "sh $W/qkx-install.sh write $name $W/stage.img"
say "$name: verifying"
sh_ "sh $W/qkx-install.sh verify $name $W/stage.img"
local verified
verified=$(sh_ "sh $W/qkx-install.sh verify $name $W/stage.img" 2>&1)
printf '%s\n' "$verified"
grep -q 'qkx_rawcp: verified' <<<"$verified" || { echo "raw verification failed: $name"; exit 1; }
sh_ "rm -f $W/stage.img"
}
@@ -106,18 +114,25 @@ else
sh_ "sh $W/qkx-install.sh writez data $W/stage.img.gz"
say 'data: verifying'
sh_ "sh $W/qkx-install.sh verify data $W/stage.img.gz"
verified=$(sh_ "sh $W/qkx-install.sh verify data $W/stage.img.gz" 2>&1)
printf '%s\n' "$verified"
grep -q 'qkx_rawcp: verified' <<<"$verified" || { echo 'raw verification failed: data'; exit 1; }
sh_ "rm -f $W/stage.img.gz"
fi
say 'collecting maps'
for name in "${IMAGES[@]}" data; do
# Read the map back from the file itself, so a stale copy cannot slip in.
adb exec-out "su -c 'sh $W/qkx-install.sh map $name'" | tr -d '\r' > "$OUT/maps/$name.map"
adb exec-out "su -c 'sh $W/qkx-install.sh map $name'" </dev/null | tr -d '\r' > "$OUT/maps/$name.map"
[ -s "$OUT/maps/$name.map" ] || { echo "empty map for $name"; exit 1; }
awk 'NF != 3 { exit 1 } { for (i=1;i<=3;i++) if ($i !~ /^[0-9a-fA-F]+$/) exit 1 }' \
"$OUT/maps/$name.map" || { echo "invalid map for $name"; head -1 "$OUT/maps/$name.map"; exit 1; }
printf '%-11s %s extents\n' "$name" "$(wc -l < "$OUT/maps/$name.map")"
done
if [ -f "$OTA/qkx-turnkey.json" ]; then
cp "$OTA/qkx-turnkey.json" "$OUT/qkx-turnkey.json"
fi
say 'installed'
sh_ "sh $W/qkx-install.sh list"
echo
+5
View File
@@ -23,4 +23,9 @@ python3 "$HERE/initramfs/build.py" --busybox "$BB" --output "$RD" \
# keeps SELinux permissive so the debug shell survives init's policy load.
"$HERE/tools/prep.sh" "$C" "$IMG" "$RD" "$OUT" androidboot.selinux=permissive qkx_selinux_permissive=1 \
hung_task_panic=1 hung_task_timeout_secs=30 softlockup_panic=1 audit=0 ${QKX_EXTRA_CMDLINE:-}
if [ -f "$OSDIR/qkx-turnkey.json" ]; then
touch "$OUT/require-stock-calibration"
else
rm -f "$OUT/require-stock-calibration"
fi
echo "payload with custom OS ready: $OUT"
+35 -6
View File
@@ -6,18 +6,27 @@ set -uo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
P=$1; shift
T=/data/local/tmp
MOD=$HERE/module/quest_kexec.ko
MOD=${QKX_LOADER_MODULE:-$HERE/module/quest_kexec.ko}
MARKER=${QKX_MARKER_MODULE:-$HERE/module/marker_read.ko}
PARAMS="execute=1 preserve_watchdog=1 watchdog_recovery=0 core_hang_control=2 \
flush_rpmh=1 suspend_syncboss=1 disconnect_qmp=0 phase_delay_ms=300 $*"
for f in "$MOD" "$HERE/module/marker_read.ko" "$P/Image" "$P/initramfs" "$P/boot.dtb"; do
for f in "$MOD" "$MARKER" "$P/Image" "$P/initramfs" "$P/boot.dtb"; do
[ -f "$f" ] || { echo "missing $f"; exit 1; }
done
INITRD=$P/initramfs
PRIVATE_INITRD=
if [ -f "$P/require-stock-calibration" ] || [ "${QKX_INHERIT_CALIBRATION:-0}" = 1 ]; then
PRIVATE_INITRD=$(mktemp)
trap 'rm -f "$PRIVATE_INITRD"' EXIT
python3 "$HERE/tools/stock-calibration-initramfs.py" "$INITRD" "$PRIVATE_INITRD" || exit 1
INITRD=$PRIVATE_INITRD
fi
# Stock hyp-assigns secure ION pages to other VMs; they stay locked after
# kexec and any access from the next kernel hangs the CPU. Collect them as
# late as possible and hand them over as one no-map reserved-memory node.
DTB=$P/boot.dtb
SECMAP=$HERE/module/ion_secmap.ko
SECMAP=${QKX_SECMAP_MODULE:-$HERE/module/ion_secmap.ko}
if [ -f "$SECMAP" ]; then
adb push "$SECMAP" $T/ion_secmap.ko >/dev/null
# Preserve Android's live hardware state by default. Stopping services
@@ -72,13 +81,33 @@ EOF
echo "reserved $(wc -l < "$P/ionsec.txt") secure ranges as $(($(echo $REG | wc -w) / 4)) blocks"
fi
# The appended calibration archive changes the initrd length. Keep /chosen
# consistent with the verified bytes staged by the loader.
if [ -n "$PRIVATE_INITRD" ]; then
if [ "$DTB" = "$P/boot.dtb" ]; then
DTB=$(mktemp --suffix=.dtb)
cp "$P/boot.dtb" "$DTB"
fi
END=$(python3 - "$DTB" "$INITRD" <<'EOF'
import pathlib, subprocess, sys
cells = subprocess.check_output(['fdtget', '-t', 'x', sys.argv[1], '/chosen', 'linux,initrd-start'], text=True).split()
start = 0
for c in cells:
start = (start << 32) | int(c, 16)
end = start + pathlib.Path(sys.argv[2]).stat().st_size
print(hex(end >> 32), hex(end & 0xffffffff))
EOF
) || exit 1
fdtput -t x "$DTB" /chosen linux,initrd-end $END || exit 1
fi
adb push "$MOD" $T/qkx.ko >/dev/null
adb push "$HERE/module/marker_read.ko" $T/qkx_marker.ko >/dev/null
adb push "$MARKER" $T/qkx_marker.ko >/dev/null
adb push "$P/Image" $T/qkx-Image >/dev/null
adb push "$P/initramfs" $T/qkx-initramfs >/dev/null
adb push "$INITRD" $T/qkx-initramfs >/dev/null
adb push "$DTB" $T/qkx-boot.dtb >/dev/null
adb shell 'su -c "sync; sync"'
for pair in "$MOD:qkx.ko" "$P/Image:qkx-Image" "$P/initramfs:qkx-initramfs" "$DTB:qkx-boot.dtb"; do
for pair in "$MOD:qkx.ko" "$P/Image:qkx-Image" "$INITRD:qkx-initramfs" "$DTB:qkx-boot.dtb"; do
l=${pair%%:*}; r=${pair##*:}
[ "$(md5sum < "$l" | cut -d' ' -f1)" = "$(adb shell "su -c 'md5sum $T/$r'" | awk '{print $1}')" ] ||
{ echo "hash mismatch: $r"; exit 1; }
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Read current stock eye calibration and add a private, volatile boot handoff.
No stock settings are written. Never put the resulting archive in source control.
"""
import argparse
import gzip
import importlib.util
import json
import os
from pathlib import Path
import stat
import subprocess
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('source', type=Path)
ap.add_argument('output', type=Path)
a = ap.parse_args()
p = subprocess.run(['adb', 'exec-out', 'su', '-c',
'/system_ext/bin/oculussetting --get eye_tracking_calibration'],
capture_output=True, timeout=30)
if p.returncode:
raise SystemExit('Cannot read stock eye calibration; refusing launch')
text = p.stdout.decode('utf-8')
try:
start = text.index('{')
obj, end = json.JSONDecoder().raw_decode(text[start:])
if not isinstance(obj, dict) or not obj.get('UserSpecificGazeParams'):
raise ValueError('Missing stock gaze parameters')
calibration = text[start:start + end].encode('utf-8')
except (ValueError, UnicodeError):
raise SystemExit('Invalid/missing stock eye calibration; refusing launch')
spec = importlib.util.spec_from_file_location('qkx_archive',
Path(__file__).resolve().parents[1] / 'initramfs/build.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
extension = module.archive([
('qkx-stock-eye-calibration.json', stat.S_IFREG | 0o600, calibration, 0, 0),
('TRAILER!!!', 0, b'', 0, 0)])
data = a.source.read_bytes() + gzip.compress(extension, mtime=0)
if len(data) > 0x200000:
raise SystemExit('Calibration handoff exceeds initramfs staging capacity')
fd = os.open(a.output, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, 'wb') as f:
f.write(data)
os.chmod(a.output, 0o600)
print('Current stock calibration captured (contents not logged)')
if __name__ == '__main__':
main()
+14
View File
@@ -0,0 +1,14 @@
#!/dev/.qkx/bin/sh
status=FAIL
trap 'echo STATUS=$status' EXIT
SS=$(pidof system_server)
N="nsenter -t $SS -m -r/proc/$SS/root"
tr '\0' '\n' < /proc/$SS/environ | grep -E '^(ANDROID_(ROOT|DATA|ART_ROOT|I18N_ROOT|TZDATA_ROOT)|BOOTCLASSPATH|DEX2OATBOOTCLASSPATH)=' > /data/local/tmp/venus-java-env.txt
while IFS= read -r kv; do export "$kv"; done < /data/local/tmp/venus-java-env.txt
export CLASSPATH=/data/local/tmp/venus-test.dex
for i in 1 2 3; do
echo VENUS_RUN=$i
$N /system/bin/app_process64 /system/bin VenusEncode /data/local/tmp/venus-ram12-$i.mp4 1920 1080 300 || exit 1
$N /system/bin/app_process64 /system/bin VenusTest /data/local/tmp/venus-ram12-$i.mp4 c2.qti.avc.decoder || exit 1
done
status=PASS
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
#!/system/bin/sh
# Runs on EVERY alternate boot, including brand-new /data. No stock mounts.
umask 077
log() { echo "qkx-defaults: $*" > /dev/kmsg; }
P=/system_ext/bin/oculuspreferences
S=/system_ext/bin/oculussetting
CAL=/dev/qkx-stock-eye-calibration.json
setprop sys.qkx.defaults pending
if [ ! -s "$CAL" ]; then
setprop sys.qkx.defaults missing_calibration
log 'Missing stock calibration handoff; refusing to report tracking ready'
exit 1
fi
# FirstTimeNux APK is absent, so it cannot race this wait on a new /data.
# Apply Meta preferences only after user 0 is active and boot completed.
while [ "$(getprop sys.boot_completed)" != 1 ]; do sleep 2; done
i=0
while ! "$P" --getc gaze_input_enabled >/dev/null 2>&1; do
i=$((i + 1)); [ "$i" -lt 90 ] || { log 'user-0 preference store unavailable'; exit 1; }
sleep 2
done
"$S" --user 0 --set first_time_nux_complete 1 || exit 1
"$S" --user 0 --set first_time_nux_pre_ota_complete 1 || exit 1
"$S" --user 0 --set first_time_nux_health_safety_complete 1 || exit 1
"$S" --user 0 --set hand_tracking_opt_in 1 || exit 1
"$P" --setc gaze_input_enabled 1 debug_eye_gaze_emit_hover_enabled true || exit 1
calibration=$(cat "$CAL")
"$P" --setc eye_tracking_calibration "$calibration" >/dev/null || exit 1
"$S" --user 0 --set eye_tracking_calibration "$calibration" >/dev/null || exit 1
unset calibration
settings put global device_provisioned 1 || exit 1
settings --user 0 put secure user_setup_complete 1 || exit 1
# Do not synthesize prox_close here: on a pristine profile VrShell handles it
# by launching an unavailable Guidebook CalibrationNuxActivity and crashes.
# Launch Home once all NUX/provisioning state is durable. Tracking calibration
# and ET/FT stabilization can safely finish in the background.
am start -n com.oculus.vrshell/.HomeActivity >/dev/null || exit 1
# Restart only tracking after all inputs are durable; leave renderer/system_server alive.
stop trackingservice
sleep 2
start trackingservice
i=0
while :; do
pid=$(pidof trackingservice)
if [ -n "$pid" ] && [ "$(getprop init.svc.trackingservice)" = running ]; then
sleep 5
[ "$pid" = "$(pidof trackingservice)" ] && break
fi
i=$((i + 1)); [ "$i" -lt 30 ] || { log 'trackingservice did not stay running'; exit 1; }
sleep 2
done
unset pid
i=0
while ! /system/bin/qkx-faceeye-request 5; do
i=$((i + 1)); [ "$i" -lt 90 ] || { log 'ET/FT fidelity request failed'; exit 1; }
sleep 2
done
setprop sys.qkx.defaults ready
log 'NUX bypass/provisioning, stock calibration, hand opt-in, gaze and ET/FT applied'
+14
View File
@@ -0,0 +1,14 @@
# Alternate-image-only bootstrap; never imported by stock init.
service qkx-defaults /system/bin/sh /system/bin/qkx-defaults
user root
group root system shell
seclabel u:r:su:s0
disabled
oneshot
on post-fs-data
# Fresh /data lacks this target of the root /vision symlink. Tracking runs
# as system:system and aborts if it cannot create /vision/insideout.
mkdir /data/misc/vision 0770 system system
restorecon_recursive /data/misc/vision
start qkx-defaults
+4
View File
@@ -0,0 +1,4 @@
# The vendor property trigger can be deferred until after post-fs-data. Start
# the existing helper at late-fs as well; the oneshot service is idempotent.
on late-fs
start qkx-start-keystore
+6
View File
@@ -0,0 +1,6 @@
#!/system/bin/sh
# KeyMint is already running when this is invoked. Give binder registration a
# short grace period, then make keystore2 available before earlyBootEnded and
# odsign incur their five-second fallback waits.
sleep 1
setprop ctl.start keystore2