From e61edbd7fbdf6dc4976a979c61cc545e04738422 Mon Sep 17 00:00:00 2001 From: YusufOzmen01 Date: Sun, 4 Oct 2026 18:09:24 +0300 Subject: [PATCH] Add turnkey Quest Pro alternate Android release tooling --- docs/KEXEC_ANDROID_WORKING_SETUP.md | 93 ++++++++++++++++--- docs/KEXEC_RAM_12GB_RESCUE.md | 71 ++++++++++++++- docs/TURNKEY_PACKAGE.md | 107 ++++++++++++++++++++++ initramfs/build.py | 4 + initramfs/init.sh | 5 +- initramfs/os/qkx-mount-os | 7 ++ module/ion_secmap.c | 3 + module/smmu_secmap.h | 104 ++++++++++++++++++++++ qkx-boot.sh | 22 +++++ qkx-install-package.sh | 102 +++++++++++++++++++++ qkx-uninstall.sh | 71 +++++++++++++++ tools/build-loader-package.py | 54 +++++++++++ tools/build-rom-package.py | 44 +++++++++ tools/build-system-package.py | 48 ++++++++++ tools/build-turnkey-images.py | 123 ++++++++++++++++++++++++++ tools/device/qkx-install.sh | 3 +- tools/disable-alt-updaters.py | 59 ++++++++++++ tools/os-install-modemfw.sh | 6 +- tools/os-install.sh | 25 ++++-- tools/os-prep.sh | 5 ++ tools/run.sh | 41 +++++++-- tools/stock-calibration-initramfs.py | 52 +++++++++++ tools/tests/venus/ram12-check.sh | 14 +++ tools/turnkey/QkxFrameworkOverlay.apk | Bin 0 -> 12624 bytes tools/turnkey/qkx-defaults | 59 ++++++++++++ tools/turnkey/qkx-defaults.rc | 14 +++ tools/turnkey/qkx-early-keystore.rc | 4 + tools/turnkey/qkx-start-keystore | 6 ++ 28 files changed, 1117 insertions(+), 29 deletions(-) create mode 100644 docs/TURNKEY_PACKAGE.md create mode 100644 module/smmu_secmap.h create mode 100755 qkx-boot.sh create mode 100755 qkx-install-package.sh create mode 100755 qkx-uninstall.sh create mode 100644 tools/build-loader-package.py create mode 100644 tools/build-rom-package.py create mode 100755 tools/build-system-package.py create mode 100755 tools/build-turnkey-images.py create mode 100644 tools/disable-alt-updaters.py create mode 100755 tools/stock-calibration-initramfs.py create mode 100644 tools/tests/venus/ram12-check.sh create mode 100644 tools/turnkey/QkxFrameworkOverlay.apk create mode 100644 tools/turnkey/qkx-defaults create mode 100644 tools/turnkey/qkx-defaults.rc create mode 100644 tools/turnkey/qkx-early-keystore.rc create mode 100644 tools/turnkey/qkx-start-keystore diff --git a/docs/KEXEC_ANDROID_WORKING_SETUP.md b/docs/KEXEC_ANDROID_WORKING_SETUP.md index b8e0c0f..da6f8d4 100644 --- a/docs/KEXEC_ANDROID_WORKING_SETUP.md +++ b/docs/KEXEC_ANDROID_WORKING_SETUP.md @@ -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: diff --git a/docs/KEXEC_RAM_12GB_RESCUE.md b/docs/KEXEC_RAM_12GB_RESCUE.md index e3e5ee5..a9145b6 100644 --- a/docs/KEXEC_RAM_12GB_RESCUE.md +++ b/docs/KEXEC_RAM_12GB_RESCUE.md @@ -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 diff --git a/docs/TURNKEY_PACKAGE.md b/docs/TURNKEY_PACKAGE.md new file mode 100644 index 0000000..d901ca5 --- /dev/null +++ b/docs/TURNKEY_PACKAGE.md @@ -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. diff --git a/initramfs/build.py b/initramfs/build.py index 3a379f1..0157a7e 100755 --- a/initramfs/build.py +++ b/initramfs/build.py @@ -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") diff --git a/initramfs/init.sh b/initramfs/init.sh index fb557ff..8d063b8 100755 --- a/initramfs/init.sh +++ b/initramfs/init.sh @@ -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" diff --git a/initramfs/os/qkx-mount-os b/initramfs/os/qkx-mount-os index 649f4af..0112586 100755 --- a/initramfs/os/qkx-mount-os +++ b/initramfs/os/qkx-mount-os @@ -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" diff --git a/module/ion_secmap.c b/module/ion_secmap.c index d3f224d..d0898dc 100644 --- a/module/ion_secmap.c +++ b/module/ion_secmap.c @@ -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); diff --git a/module/smmu_secmap.h b/module/smmu_secmap.h new file mode 100644 index 0000000..6c29e37 --- /dev/null +++ b/module/smmu_secmap.h @@ -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 +#include +#include +#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); +} diff --git a/qkx-boot.sh b/qkx-boot.sh new file mode 100755 index 0000000..8b753ee --- /dev/null +++ b/qkx-boot.sh @@ -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.' diff --git a/qkx-install-package.sh b/qkx-install-package.sh new file mode 100755 index 0000000..e7e7b1f --- /dev/null +++ b/qkx-install-package.sh @@ -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 || 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" diff --git a/qkx-uninstall.sh b/qkx-uninstall.sh new file mode 100755 index 0000000..da22b3b --- /dev/null +++ b/qkx-uninstall.sh @@ -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.' diff --git a/tools/build-loader-package.py b/tools/build-loader-package.py new file mode 100644 index 0000000..bd5d2a3 --- /dev/null +++ b/tools/build-loader-package.py @@ -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() diff --git a/tools/build-rom-package.py b/tools/build-rom-package.py new file mode 100644 index 0000000..0110ba1 --- /dev/null +++ b/tools/build-rom-package.py @@ -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() diff --git a/tools/build-system-package.py b/tools/build-system-package.py new file mode 100755 index 0000000..2c8b3cd --- /dev/null +++ b/tools/build-system-package.py @@ -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() diff --git a/tools/build-turnkey-images.py b/tools/build-turnkey-images.py new file mode 100755 index 0000000..3053121 --- /dev/null +++ b/tools/build-turnkey-images.py @@ -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() diff --git a/tools/device/qkx-install.sh b/tools/device/qkx-install.sh index 2cd3f52..69d3ca1 100755 --- a/tools/device/qkx-install.sh +++ b/tools/device/qkx-install.sh @@ -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 diff --git a/tools/disable-alt-updaters.py b/tools/disable-alt-updaters.py new file mode 100644 index 0000000..30ccb0b --- /dev/null +++ b/tools/disable-alt-updaters.py @@ -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) diff --git a/tools/os-install-modemfw.sh b/tools/os-install-modemfw.sh index f4691eb..3659dcc 100755 --- a/tools/os-install-modemfw.sh +++ b/tools/os-install-modemfw.sh @@ -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 diff --git a/tools/os-install.sh b/tools/os-install.sh index 00aa2ef..6916f04 100755 --- a/tools/os-install.sh +++ b/tools/os-install.sh @@ -16,7 +16,8 @@ OTA=${1:?usage: os-install.sh [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'" "$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 diff --git a/tools/os-prep.sh b/tools/os-prep.sh index aeffe3f..667c4b9 100755 --- a/tools/os-prep.sh +++ b/tools/os-prep.sh @@ -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" diff --git a/tools/run.sh b/tools/run.sh index 8b00ae7..4cb165e 100755 --- a/tools/run.sh +++ b/tools/run.sh @@ -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; } diff --git a/tools/stock-calibration-initramfs.py b/tools/stock-calibration-initramfs.py new file mode 100755 index 0000000..df8a204 --- /dev/null +++ b/tools/stock-calibration-initramfs.py @@ -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() diff --git a/tools/tests/venus/ram12-check.sh b/tools/tests/venus/ram12-check.sh new file mode 100644 index 0000000..d0ea8da --- /dev/null +++ b/tools/tests/venus/ram12-check.sh @@ -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 diff --git a/tools/turnkey/QkxFrameworkOverlay.apk b/tools/turnkey/QkxFrameworkOverlay.apk new file mode 100644 index 0000000000000000000000000000000000000000..66f98b46a64c75f51b6449eafd25b52806e2ad3e GIT binary patch literal 12624 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 literal 0 HcmV?d00001 diff --git a/tools/turnkey/qkx-defaults b/tools/turnkey/qkx-defaults new file mode 100644 index 0000000..e598d09 --- /dev/null +++ b/tools/turnkey/qkx-defaults @@ -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' diff --git a/tools/turnkey/qkx-defaults.rc b/tools/turnkey/qkx-defaults.rc new file mode 100644 index 0000000..eeb28cd --- /dev/null +++ b/tools/turnkey/qkx-defaults.rc @@ -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 diff --git a/tools/turnkey/qkx-early-keystore.rc b/tools/turnkey/qkx-early-keystore.rc new file mode 100644 index 0000000..3d8da42 --- /dev/null +++ b/tools/turnkey/qkx-early-keystore.rc @@ -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 diff --git a/tools/turnkey/qkx-start-keystore b/tools/turnkey/qkx-start-keystore new file mode 100644 index 0000000..9c4d166 --- /dev/null +++ b/tools/turnkey/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