mirror of
https://github.com/YusufOzmen01/quest-kexec.git
synced 2026-10-26 19:37:39 +00:00
Add turnkey Quest Pro alternate Android release tooling
This commit is contained in:
28 files changed
+1117
-29
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.'
|
||||
Executable
+102
@@ -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"
|
||||
Executable
+71
@@ -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.'
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+48
@@ -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()
|
||||
Executable
+123
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
Executable
+52
@@ -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()
|
||||
@@ -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.
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user