commit 4537fb948f523c8a28debd955e9e33cc16da5bb4 Author: yusuf Date: Sat Sep 26 21:36:07 2026 +0300 Initial Quest Pro ARM64 kexec release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae888cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Builds and device captures +/out/ +/build/ +/captured/ +/runs/ +*.ko +*.o +*.mod +*.mod.c +*.cmd +Module.symvers +modules.order + +# Proprietary/device-specific inputs +boot*.img +Image +*.dtb +*.fdt +*.config +cmdline +iomem + +# Editors/Python +__pycache__/ +*.pyc +*~ +.vscode/ +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8f218a9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +- Standalone Quest Pro ARM64 kexec loader repository. +- Reproducible ECM-only BusyBox initramfs with DHCP and telnet shell. +- Device capture, payload preparation, hash-verified run and retained-log tools. +- Kernel patch series matching the maintained `qkx-kexec` branch. +- Validated with 8 CPUs, DRM, USB, QMP, syncboss, camera CPAS/CDM and SMB5. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5655f18 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing + +Keep hardware/OS specifics explicit in commit messages. Do not commit boot +images, firmware, device captures, serial numbers, vbmeta digests, proprietary +binaries, or files pulled from partitions. Generated payloads belong under +`out/` and are ignored. + +A hardware test report should state the Android build/kernel, target commit, +loader parameters, target command line additions, power/cable state, and whether +retained/pstore evidence survived. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..227b449 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..45e9043 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Convenience targets. See README.md for the full procedure. +# make module KDIR=... [LOCALVERSION=...] +# make initramfs +OUT ?= out + +.PHONY: all module initramfs clean +all: module initramfs + +module: + $(MAKE) -C module KDIR=$(KDIR) LOCALVERSION=$(LOCALVERSION) + +$(OUT)/busybox: + initramfs/busybox.sh $@ + +initramfs: $(OUT)/busybox + python3 initramfs/build.py --busybox $(OUT)/busybox --output $(OUT)/initramfs.gz + +clean: + -$(MAKE) -C module clean KDIR=$(KDIR) + rm -rf $(OUT) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f54b8a3 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# Quest Pro kexec + +Experimental ARM64 kexec loader and RAM-only Linux environment for Meta Quest Pro +(Seacliff/Kona, Linux 4.19). It hands execution from the rooted Android kernel to +a patched Quest kernel entirely from RAM. No partition is flashed or mounted. + +The included initramfs exposes a BusyBox shell over USB Ethernet (ECM): + +- target: `10.42.0.2` +- host: `10.42.0.1` (offered by target DHCP) +- shell: `telnet 10.42.0.2` +- no USB serial functions and no block-device mounts + +## Status + +Validated on Quest Pro OS build `51503870024400340`, Android kernel +`4.19.325-cip128-st12-g49638c7a8637`, Meta source commit +`fa2e480a85c6adbcc73dfc4f0ab0728781e14584`. + +Working in the RAM-only target: 8 CPUs, DRM/panels, USB ECM, AOP QMP handoff, +syncboss, camera CPAS/CDM, SPMI and SMB5 charging. The target kernel patches are +also maintained on the `qkx-kexec` branch of +[`YusufOzmen01/oculus-linux-kernel`](https://github.com/YusufOzmen01/oculus-linux-kernel). +Patch files are mirrored under `kernel/patches/` for review and portability. + +## Safety + +This is research software. A failed handoff can freeze or reboot the headset. +Keep the stock boot image available and use only on a rooted device you can +recover. The supplied scripts only write temporary files under +`/data/local/tmp`; they do not flash, format or mount partitions. + +Do **not** use a loader module built for a different running Android kernel. +Module ABI mismatches can crash Android during an irreversible shutdown. + +## Host requirements + +Linux host with: + +- `adb`, root access through `adb shell 'su -c ...'` +- Clang/LLD and an `aarch64-linux-gnu-` toolchain +- Python 3, `dtc`, `fdtput`, `curl`, `make`, `cpio`, `gzip` +- NetworkManager or another way to configure the ECM host interface + +## Quick start + +See [docs/build.md](docs/build.md) for complete kernel/module build steps. +Assuming the patched target `Image` and matching modules already exist: + +```sh +# Build static BusyBox and the ECM-only initramfs +make initramfs + +# Build loader against the *running Android kernel's* completed build tree +make module KDIR=/path/to/android-kernel-out \ + LOCALVERSION=-g49638c7a8637 + +# Capture runtime DT, iomem, cmdline, config (read-only on device) +tools/capture.sh out/captured + +# Prepare a payload using the patched target kernel +tools/prep.sh out/captured /path/to/target/Image \ + out/initramfs.gz out/payload + +# Freshly pushes and verifies every file before jumping +tools/run.sh out/payload +``` + +After USB re-enumerates, NetworkManager should obtain `10.42.0.1` from the +target. If not: + +```sh +nmcli device connect +# or: sudo ip addr add 10.42.0.1/24 dev +telnet 10.42.0.2 +``` + +Return to Android while the target is running: + +```sh +python3 tools/qkx_sh.py 'echo r > /proc/qkx_bootdone' +``` + +After Android returns, retrieve a retained target log: + +```sh +tools/read-log.sh +``` + +## Repository layout + +- `module/` — kexec loader, ARM64 transition code, retained-log reader +- `initramfs/` — ECM-only PID 1 and reproducible BusyBox/initramfs builders +- `tools/` — device capture, payload preparation, run and shell helpers +- `kernel/patches/` — patches required by the target kernel +- `docs/` — detailed build, usage and architecture notes + +## License + +GPL-2.0. See `LICENSE`. BusyBox is downloaded from upstream and built locally; +its binary and source are not committed. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6bab5f7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,8 @@ +# Security + +The loader deliberately crosses a kernel trust boundary and executes a second +kernel without a bootloader verification step. Use it only on hardware you own. +Do not distribute boot images, firmware, device keys or captures. + +Report security-sensitive issues privately to the repository owner rather than +posting device identifiers or logs containing secrets in a public issue. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..482d286 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,50 @@ +# Architecture and handoff notes + +## Memory layout + +| Payload | Physical address | +|---|---:| +| target Image | `0x90080000` | +| initramfs | `0x93000000` | +| DTB | `0x93200000` | +| retained ring | `0x9ba80000`, 64 KiB | + +The loader stages segments in ordinary pages, freezes Android userspace, +quiesces GPU, syncboss and DWC3, calls the normal device shutdown path, offlines +secondary CPUs, flushes RPMh, copies the payload through an identity-mapped +ARM64 transition and enters the target at EL1. + +## Important inherited-state fixes + +The target patch series contains the fixes established during bring-up: + +- SMP/GIC and apps-SMMU inherited-state tolerance +- msm_bus/RPMh QoS handling (Quest's nonexistent UFS-card master is skipped) +- display GDSCs, Display CC, SDE RSC and DRM handoff +- DWC3/USB PHY gadget handoff +- AOP QMP link adoption and inherited QDSS vote adoption +- syncboss quiesce through Android's PM callback +- SPMI stale interrupt disable +- CPAS/CCI child-node lookups and camera/CDM diagnostics +- SMB5 probe with unsafe synthetic initial IRQ callbacks skipped +- retained log plus boot watchdog diagnostics + +## Runtime tree requirement + +Use the runtime tree reconstructed from `/proc/device-tree`, not the original +blob in `/sys/firmware/fdt`. The original boot FDT caused a cold reset in a +validated comparison test. `tools/capture.sh` handles the correct conversion. + +## QMP + +Never take the AP-to-AOP link down during handoff. Loader-side QMP disconnect +caused an immediate PMIC reset. The target adopts the inherited physical link, +recycles only the logical channel and imports the negotiated mailbox metadata. +The required target command line option is `qkx_qmp_adopt=1`. + +## SMB5 + +Android's normal `device_shutdown()` already calls `smb5_shutdown()`. The target +must not manually call the 11 synthetic initial-status interrupt handlers after +kexec; they reset or lock the PMIC/USB-C path. The patched driver defaults to +skipping those calls. Charging and real plug interrupts remain functional. diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..7a5f789 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,70 @@ +# Building + +There are two kernels involved: + +1. **Android kernel** currently running on the headset. It is used only to build + `quest_kexec.ko`; source, configuration, release suffix and `Module.symvers` + must match the device exactly. +2. **Target kernel** entered by kexec. It is the Meta Quest Pro kernel with the + patches in `kernel/patches/` (or branch `qkx-kexec` in the companion fork). + +## Android loader ABI tree + +For OS build `51503870024400340`, use Meta commit `fa2e480a85...`, copy the +headset's `/proc/config.gz` to the output `.config`, disable +`CONFIG_LOCALVERSION_AUTO`, and build `Image modules` with local version +`-g49638c7a8637`. A full module build is required to produce correct symbol +CRCs in `Module.symvers`. + +The vendor audio tree needs its pinctrl include directory in `KCFLAGS`: + +```sh +SRC=/path/to/meta-kernel-at-fa2e480a85 +OUT=/path/to/android-out +adb exec-out 'su -c "zcat /proc/config.gz"' > "$OUT/.config" +$SRC/scripts/config --file "$OUT/.config" --disable LOCALVERSION_AUTO +make -C "$SRC" O="$OUT" ARCH=arm64 CC=clang LD=ld.lld HOSTCC=clang \ + CROSS_COMPILE=aarch64-linux-gnu- CLANG_TRIPLE=aarch64-linux-gnu- \ + LOCALVERSION=-g49638c7a8637 olddefconfig +make -C "$SRC" O="$OUT" ARCH=arm64 CC=clang LD=ld.lld HOSTCC=clang \ + CROSS_COMPILE=aarch64-linux-gnu- CLANG_TRIPLE=aarch64-linux-gnu- \ + LOCALVERSION=-g49638c7a8637 KCFLAGS=-I$SRC/drivers/pinctrl -j8 Image modules +make module KDIR="$OUT" LOCALVERSION=-g49638c7a8637 +modinfo -F vermagic module/quest_kexec.ko +``` + +The reported vermagic must begin with the output of `adb shell uname -r`. + +## Target kernel + +Clone `YusufOzmen01/oculus-linux-kernel`, then check out `qkx-kexec`. Or apply +all patches in order to upstream Quest Pro commit `42dc87a978`: + +```sh +git am /path/to/quest-pro-kexec/kernel/patches/*.patch +``` + +Start from the device config and make these configuration changes: + +```text +CONFIG_LOCALVERSION_AUTO=n +CONFIG_ION_POOL_AUTO_REFILL=n +CONFIG_USB_CONFIGFS_ECM=y +CONFIG_DEVTMPFS=y +CONFIG_PRINTK_TIME=y +CONFIG_LOG_BUF_SHIFT=20 +``` + +Build a raw ARM64 `Image`. The repository's staging layout requires text offset +`0x80000`, a relocatable little-endian 4 KiB-page image, maximum memory size +`0x2f80000`, and maximum initramfs/DTB size 2 MiB each. + +## BusyBox/initramfs + +```sh +make initramfs +``` + +`initramfs/busybox.sh` downloads BusyBox 1.36.1 from busybox.net, verifies its +published SHA-256, then cross-builds it static. `initramfs/build.py` verifies the +binary is static ARM64 and generates deterministic gzip/newc output. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..13c6115 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,44 @@ +# Usage and troubleshooting + +## Normal run + +1. Charge the headset and boot rooted Android. +2. Build loader for that exact Android kernel. +3. Build patched target kernel and initramfs. +4. Run `tools/capture.sh out/captured` after every OS/device-tree change. +5. Prepare and execute as shown in the README. + +Each execution freshly pushes Image, initramfs and DTB, runs `sync` twice and +verifies MD5 hashes before inserting the loader. + +## Target shell + +The initramfs advertises one USB ECM function (VID:PID `1d6b:0105`) with fixed +MAC addresses. The interface name differs from old ACM+ECM payloads because ECM +is now interface zero. Prefer finding it by MAC or `ip link` rather than hard +coding its name. + +`udhcpd` offers only `10.42.0.1/24`; it advertises no gateway or DNS, so the host +should not route Internet traffic through the headset. + +## Reset classification + +- Retained marker exists: target reached far enough to initialize the ring and + returned via a DRAM-preserving warm reset. +- Marker is zero and pstore contains `quest_kexec: native CPU shutdown failed`: + loader panic inside Android. +- Marker is zero with no pstore evidence: PMIC hard/cold reset or very early + failure; do not attribute it to the last target probe without a checkpoint. + +`tools/read-log.sh` reads both retained-ring and pstore evidence after Android +returns. A stale marker is cleared before every run. + +## Common failures + +- **Module won't load / disagrees about symbol version:** rebuild against the + exact running Android source/config/release; never force-load it. +- **Gadget exists but target is unreachable:** ask NetworkManager to connect or + manually assign `10.42.0.1/24` to the new ECM interface. +- **No gadget and Android returns:** retrieve retained logs immediately. +- **Hard lockup:** force reboot may erase all evidence. Avoid repeating an + unchanged payload; add a checkpoint or narrow the boundary first. diff --git a/initramfs/build.py b/initramfs/build.py new file mode 100755 index 0000000..730fd88 --- /dev/null +++ b/initramfs/build.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Pack a static ARM64 BusyBox and init.sh into a gzip newc initramfs.""" +import argparse +import gzip +import stat +import struct +from pathlib import Path + + +def archive(entries): + data = bytearray() + for ino, (name, mode, contents, major, minor) in enumerate(entries, 1): + name = name.encode() + b"\0" + fields = (ino, mode, 0, 0, 1, 0, len(contents), 0, 0, major, minor, len(name), 0) + data += b"070701" + "".join(f"{x:08x}" for x in fields).encode() + name + data += b"\0" * (-len(data) % 4) + data += contents + data += b"\0" * (-len(data) % 4) + data += b"\0" * (-len(data) % 512) + return bytes(data) + + +def check_static_arm64(path): + b = path.read_bytes() + if b[:6] != b"\x7fELF\x02\x01" or struct.unpack_from(" 0x200000: + raise SystemExit(f"initramfs is {len(data)} bytes; the staging layout allows 2 MiB") + a.output.parent.mkdir(parents=True, exist_ok=True) + a.output.write_bytes(data) + print(f"{a.output}: {len(data)} bytes") + + +if __name__ == "__main__": + main() diff --git a/initramfs/busybox.sh b/initramfs/busybox.sh new file mode 100755 index 0000000..20be428 --- /dev/null +++ b/initramfs/busybox.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Download, verify and build a static ARM64 BusyBox. +# Usage: busybox.sh +set -euo pipefail +VER=1.36.1 +SHA=b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314 +OUT=$(realpath -m "$1") +WORK=$(dirname "$OUT")/busybox-build +CROSS=${CROSS_COMPILE:-aarch64-linux-gnu-} +mkdir -p "$WORK" +cd "$WORK" +[ -f busybox-$VER.tar.bz2 ] || + curl -fL -o busybox-$VER.tar.bz2 https://busybox.net/downloads/busybox-$VER.tar.bz2 +echo "$SHA busybox-$VER.tar.bz2" | sha256sum -c - +rm -rf busybox-$VER +tar xjf busybox-$VER.tar.bz2 +cd busybox-$VER +M="make ARCH=arm64 CROSS_COMPILE=$CROSS" +$M defconfig >/dev/null +# Static, and drop tc: it doesn't build against current kernel headers. +sed -i 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/; s/^CONFIG_TC=y/# CONFIG_TC is not set/' .config +$M oldconfig /dev/null 2>&1 || true +grep -q '^CONFIG_STATIC=y' .config +$M -j"$(nproc)" >/dev/null +cp busybox "$OUT" +echo "$OUT" diff --git a/initramfs/init.sh b/initramfs/init.sh new file mode 100755 index 0000000..8471c22 --- /dev/null +++ b/initramfs/init.sh @@ -0,0 +1,75 @@ +#!/bin/busybox sh +# RAM-only init: USB Ethernet (ECM) gadget with a BusyBox telnet shell. +# The headset is 10.42.0.2; udhcpd hands the host 10.42.0.1. +# Nothing here mounts a block device. +export PATH=/bin:/sbin +export HOME=/root +export TERM=vt100 +/bin/busybox --install -s /bin +mount -t proc proc /proc +mount -t sysfs sysfs /sys +mount -t devtmpfs devtmpfs /dev +mkdir -p /dev/pts /config /tmp +mount -t devpts devpts /dev/pts +mount -t configfs configfs /config +mount -t tmpfs tmpfs /tmp +exec >/dev/kmsg 2>&1 + +log() { echo "qkx-init: $*"; } +# /proc/qkx_bootdone comes from the target kernel patches: 'd' disarms the +# boot watchdog, 'k' is a keepalive, 'r' forces a log-preserving warm reset. +bootdone() { [ -w /proc/qkx_bootdone ] && echo "$1" > /proc/qkx_bootdone; } + +log "reached $(uname -r)" + +g=/config/usb_gadget/qkx +mkdir -p "$g/strings/0x409" "$g/configs/c.1/strings/0x409" "$g/functions/ecm.usb0" +echo 0x1d6b > "$g/idVendor" +echo 0x0105 > "$g/idProduct" +echo QKX-RAM > "$g/strings/0x409/serialnumber" +echo 'quest-pro-kexec' > "$g/strings/0x409/manufacturer" +echo 'RAM-only USB Ethernet shell' > "$g/strings/0x409/product" +echo 'ECM' > "$g/configs/c.1/strings/0x409/configuration" +echo 250 > "$g/configs/c.1/MaxPower" +# Fixed MACs keep the host interface name stable across boots. +echo 02:51:4b:58:00:02 > "$g/functions/ecm.usb0/dev_addr" +echo 02:51:4b:58:00:01 > "$g/functions/ecm.usb0/host_addr" +ln -s "$g/functions/ecm.usb0" "$g/configs/c.1/ecm.usb0" + +udc= +for _ in 1 2 3 4 5 6 7 8 9 10; do + for c in /sys/class/udc/*; do + [ -e "$c" ] || continue + echo "${c##*/}" > "$g/UDC" && udc=$c && break 2 + done + log "waiting for UDC" + bootdone k + sleep 1 +done +if [ -z "$udc" ]; then + log "no UDC; forcing warm reset so the log survives" + bootdone r + while :; do sleep 60; done +fi +log "bound ${udc##*/}" +# Keep the DWC3 glue device out of runtime suspend. +echo on > "$udc/device/../power/control" 2>/dev/null + +ifconfig lo 127.0.0.1 up +ifconfig usb0 10.42.0.2 netmask 255.255.255.0 up +cat > /tmp/udhcpd.conf < +Date: Sat, 26 Sep 2026 20:17:50 +0300 +Subject: [PATCH 1/3] qkx: kexec target patches (rebased from fa2e480a85) + +--- + arch/arm64/kernel/head.S | 5 + + drivers/base/dd.c | 94 +++-- + drivers/clk/qcom/clk-aop-qmp.c | 16 +- + drivers/clk/qcom/dispcc-kona.c | 34 +- + drivers/clk/qcom/gdsc-regulator.c | 15 +- + drivers/cpufreq/cpufreq_stats.c | 35 +- + drivers/iommu/arm-smmu.c | 12 +- + drivers/irqchip/irq-gic-v3.c | 19 ++ + drivers/mailbox/msm_qmp.c | 132 ++++++- + drivers/misc/qseecom.c | 8 +- + drivers/of/device.c | 17 +- + drivers/power/reset/msm-poweroff.c | 27 ++ + .../soc/qcom/msm_bus/msm_bus_fabric_rpmh.c | 113 +++++- + .../soc/qcom/msm_bus/msm_bus_proxy_client.c | 8 +- + drivers/usb/dwc3/dwc3-msm.c | 33 +- + drivers/usb/dwc3/gadget.c | 91 +++++ + drivers/usb/phy/phy-msm-ssusb-qmp.c | 8 + + drivers/usb/phy/phy.c | 22 +- + drivers/watchdog/watchdog_core.c | 5 +- + init/main.c | 321 +++++++++++++++++- + net/ipv4/ip_output.c | 1 + + .../camera/drivers/cam_cdm/cam_cdm_hw_core.c | 50 +++ + .../camera/drivers/cam_cpas/cam_cpas_soc.c | 9 +- + .../cam_sensor_module/cam_cci/cam_cci_soc.c | 8 +- + .../camera/drivers/cam_smmu/cam_smmu_api.c | 3 + + techpack/display/msm/dsi/dsi_ctrl.c | 24 +- + techpack/display/msm/sde_power_handle.c | 38 ++- + techpack/display/msm/sde_rsc.c | 3 +- + 28 files changed, 1033 insertions(+), 118 deletions(-) + +diff --git a/arch/arm64/kernel/head.S b/arch/arm64/kernel/head.S +index 29f67c995d..072ae6663a 100644 +--- a/arch/arm64/kernel/head.S ++++ b/arch/arm64/kernel/head.S +@@ -117,6 +117,11 @@ pe_header: + * current RELR displacement + */ + ENTRY(stext) ++ /* The replacement boot must not inherit or re-arm the AP watchdog. */ ++ movz x9, #0 ++ movk x9, #0x17c1, lsl #16 ++ str wzr, [x9, #0x08] ++ dsb sy + bl preserve_boot_args + bl el2_setup // Drop to EL1, w0=cpu_boot_mode + adrp x23, __PHYS_OFFSET +diff --git a/drivers/base/dd.c b/drivers/base/dd.c +index 3bc011d40f..2f822cdc3a 100644 +--- a/drivers/base/dd.c ++++ b/drivers/base/dd.c +@@ -202,14 +202,45 @@ void device_unblock_probing(void) + driver_deferred_probe_trigger(); + } + +-static void enable_trigger_defer_cycle(void) ++/* qkx: explicit diagnostic kick enables slow per-probe tracing. */ ++bool qkx_probe_trace; ++/* Devices known to be unsafe or blocking when inherited across kexec. */ ++static char *qkx_probe_skip = ++ "c440000.qcom,spmi:qcom,pm8150b@2:qcom,qpnp-smb5|" ++ "ac4d000.qcom,cpas-cdm0"; ++core_param(qkx_probe_skip, qkx_probe_skip, charp, 0444); ++ ++static bool qkx_probe_is_skipped(const char *name) ++{ ++ const char *p = qkx_probe_skip; ++ size_t n; ++ ++ while (p && *p) { ++ n = strcspn(p, "|"); ++ if (strlen(name) == n && !strncmp(name, p, n)) ++ return true; ++ p += n; ++ if (*p == '|') ++ p++; ++ } ++ return false; ++} ++ ++void qkx_deferred_probe_kick(void) + { ++ pr_emerg("qkx: userspace kick of deferred probing\n"); ++ qkx_probe_trace = true; + driver_deferred_probe_enable = true; + driver_deferred_probe_trigger(); +- /* +- * Sort as many dependencies as possible before the next initcall +- * level ++} ++ ++static void enable_trigger_defer_cycle(void) ++{ ++ /* The inherited-state hazards are parked by qkx_probe_skip, so run the ++ * vendor kernel's normal dependency-resolution cycle at each sync level. + */ ++ driver_deferred_probe_enable = true; ++ driver_deferred_probe_trigger(); + flush_work(&deferred_probe_work); + } + +@@ -311,16 +342,9 @@ static int deferred_probe_enable_fn(void) + deferred_devices = debugfs_create_file("devices_deferred", 0444, NULL, + NULL, &deferred_devs_fops); + +- /* Enable deferred probing for all time */ +- enable_trigger_defer_cycle(); +- initcalls_done = true; +- +- /* +- * Trigger deferred probe again, this time we won't defer anything +- * that is optional +- */ ++ driver_deferred_probe_enable = true; + driver_deferred_probe_trigger(); +- flush_work(&deferred_probe_work); ++ initcalls_done = true; + + if (deferred_probe_timeout > 0) { + schedule_delayed_work(&deferred_probe_timeout_work, +@@ -481,8 +505,14 @@ static int really_probe(struct device *dev, struct device_driver *drv) + int local_trigger_count = atomic_read(&deferred_trigger_count); + bool test_remove = IS_ENABLED(CONFIG_DEBUG_TEST_DRIVER_REMOVE) && + !drv->suppress_bind_attrs; ++ bool qkx_trace = strstr(dev_name(dev), "ssusb") || ++ strstr(dev_name(dev), "ssphy") || ++ strstr(dev_name(dev), "dwc3"); + + if (defer_all_probes) { ++ if (qkx_trace) ++ dev_info(dev, "qkx: defer_all_probes for %s\n", ++ drv->name); + /* + * Value of defer_all_probes can be set only by + * device_defer_all_probes_enable() which, in turn, will call +@@ -496,8 +526,12 @@ static int really_probe(struct device *dev, struct device_driver *drv) + ret = device_links_check_suppliers(dev); + if (ret == -EPROBE_DEFER) + driver_deferred_probe_add_trigger(dev, local_trigger_count); +- if (ret) ++ if (ret) { ++ if (qkx_trace) ++ dev_info(dev, "qkx: suppliers ret=%d drv=%s\n", ++ ret, drv->name); + return ret; ++ } + + atomic_inc(&probe_count); + pr_debug("bus: '%s': %s: probing driver %s with device %s\n", +@@ -513,12 +547,18 @@ static int really_probe(struct device *dev, struct device_driver *drv) + + /* If using pinctrl, bind pins now before probing */ + ret = pinctrl_bind_pins(dev); +- if (ret) ++ if (ret) { ++ if (qkx_trace) ++ dev_info(dev, "qkx: pinctrl ret=%d\n", ret); + goto pinctrl_bind_failed; ++ } + + ret = dma_configure(dev); +- if (ret) ++ if (ret) { ++ if (qkx_trace) ++ dev_info(dev, "qkx: dma_configure ret=%d\n", ret); + goto probe_failed; ++ } + + if (driver_sysfs_add(dev)) { + printk(KERN_ERR "%s: driver_sysfs_add(%s) failed\n", +@@ -532,15 +572,25 @@ static int really_probe(struct device *dev, struct device_driver *drv) + goto probe_failed; + } + ++ if (qkx_probe_is_skipped(dev_name(dev))) { ++ dev_warn_once(dev, "qkx: probe parked for kexec safety\n"); ++ ret = -EPROBE_DEFER; ++ goto probe_failed; ++ } ++ if (qkx_probe_trace) { ++ pr_emerg("qkx: probe %s drv=%s\n", dev_name(dev), drv->name); ++ /* Diagnostic mode only: let the line reach the host before MMIO. */ ++ msleep(200); ++ } + if (dev->bus->probe) { + ret = dev->bus->probe(dev); +- if (ret) +- goto probe_failed; + } else if (drv->probe) { + ret = drv->probe(dev); +- if (ret) +- goto probe_failed; + } ++ if (qkx_probe_trace) ++ pr_emerg("qkx: probe %s ret=%d\n", dev_name(dev), ret); ++ if (ret) ++ goto probe_failed; + + if (test_remove) { + test_remove = false; +@@ -591,11 +641,15 @@ static int really_probe(struct device *dev, struct device_driver *drv) + switch (ret) { + case -EPROBE_DEFER: + /* Driver requested deferred probing */ ++ if (qkx_trace) ++ dev_info(dev, "qkx: probe deferred (ret=%d)\n", ret); + dev_dbg(dev, "Driver %s requests probe deferral\n", drv->name); + driver_deferred_probe_add_trigger(dev, local_trigger_count); + break; + case -ENODEV: + case -ENXIO: ++ if (qkx_trace) ++ dev_info(dev, "qkx: probe rejects match (ret=%d)\n", ret); + pr_debug("%s: probe of %s rejects match %d\n", + drv->name, dev_name(dev), ret); + break; +diff --git a/drivers/clk/qcom/clk-aop-qmp.c b/drivers/clk/qcom/clk-aop-qmp.c +index 0f7b77d012..f235b850c3 100644 +--- a/drivers/clk/qcom/clk-aop-qmp.c ++++ b/drivers/clk/qcom/clk-aop-qmp.c +@@ -76,6 +76,7 @@ struct clk_aop_qmp { + }; + + static DEFINE_MUTEX(clk_aop_lock); ++extern bool qkx_qmp_inherited_ready; + + static unsigned long clk_aop_qmp_recalc_rate(struct clk_hw *hw, + unsigned long parent_rate) +@@ -329,9 +330,18 @@ static int aop_qmp_clk_probe(struct platform_device *pdev) + * Proxy vote on the QDSS clock. This is needed to avoid issues with + * excessive requests on the QMP layer during the QDSS driver probe. + */ +- ret = clk_aop_qmp_prepare(&qdss_qmp_clk.hw); +- if (ret < 0) +- goto fail; ++ if (qkx_qmp_inherited_ready) { ++ /* Android's CLK_ENABLE_HAND_OFF QDSS proxy vote remains active. ++ * Sending the duplicate first vote after adopting its QMP link is ++ * not acknowledged by AOP, exactly what enabled is meant to avoid. ++ */ ++ qdss_qmp_clk.enabled = true; ++ dev_info(&pdev->dev, "qkx: adopting inherited QDSS proxy vote\n"); ++ } else { ++ ret = clk_aop_qmp_prepare(&qdss_qmp_clk.hw); ++ if (ret < 0) ++ goto fail; ++ } + + for (i = 0; i < num_clks; i++) { + if (!aop_qmp_clk_hws[i]) +diff --git a/drivers/clk/qcom/dispcc-kona.c b/drivers/clk/qcom/dispcc-kona.c +index 7fe18865f5..91e1c511be 100644 +--- a/drivers/clk/qcom/dispcc-kona.c ++++ b/drivers/clk/qcom/dispcc-kona.c +@@ -1532,6 +1532,27 @@ static int disp_cc_kona_probe(struct platform_device *pdev) + return PTR_ERR(regmap); + } + ++ /* ++ * qkx: disp_cc and gcc are both subsys_initcall; if disp_cc's link ++ * order runs first, devm_clk_get("cfg_ahb_clk") returns -EPROBE_DEFER ++ * and (with deferred-probe retries disabled kernel-wide in this ++ * diagnostic build) that failure was permanent. ++ * ++ * A local msleep-based retry loop was tried here and made things ++ * worse: subsys_initcall runs single-threaded, so blocking inside ++ * disp_cc's own probe also blocks every later subsys_initcall from ++ * running at all -- including gcc-kona's, which is the one thing ++ * disp_cc is waiting on. Confirmed directly: dmesg showed a ~20s gap ++ * with zero log lines from any driver, ending exactly at the retry ++ * loop's own deadline, with gcc-kona's "Registered GCC clocks" ++ * appearing only after. The loop was waiting for something it was ++ * itself preventing from happening. ++ * ++ * The actual fix is below: disp_cc_kona_init() is registered at ++ * fs_initcall, one level after subsys_initcall, so gcc-kona (and the ++ * RPMh regulator framework, which registers even earlier at ++ * arch_initcall) are guaranteed to have already run. ++ */ + clk = devm_clk_get(&pdev->dev, "cfg_ahb_clk"); + if (IS_ERR(clk)) { + if (PTR_ERR(clk) != -EPROBE_DEFER) +@@ -1550,8 +1571,14 @@ static int disp_cc_kona_probe(struct platform_device *pdev) + + dispcc_bus_id = msm_bus_scale_register_client(&clk_debugfs_scale_table); + if (!dispcc_bus_id) { +- dev_err(&pdev->dev, "Unable to register for bw voting\n"); +- return -EPROBE_DEFER; ++ /* ++ * qkx: after kexec msm_bus may never come up (deferred retries ++ * are disabled in this diagnostic kernel), which would leave ++ * every display clock missing. The vote only serves debugfs ++ * clock measurement and all users check for a zero id. ++ */ ++ dev_warn(&pdev->dev, ++ "qkx: bw voting unavailable; continuing without it\n"); + } + for (i = 0; i < ARRAY_SIZE(disp_cc_kona_clocks); i++) + if (disp_cc_kona_clocks[i]) +@@ -1587,7 +1614,8 @@ static int __init disp_cc_kona_init(void) + { + return platform_driver_register(&disp_cc_kona_driver); + } +-subsys_initcall(disp_cc_kona_init); ++/* qkx: was subsys_initcall; see the ordering comment in disp_cc_kona_probe(). */ ++fs_initcall(disp_cc_kona_init); + + static void __exit disp_cc_kona_exit(void) + { +diff --git a/drivers/clk/qcom/gdsc-regulator.c b/drivers/clk/qcom/gdsc-regulator.c +index ee8b6a2cde..f3fe105da1 100644 +--- a/drivers/clk/qcom/gdsc-regulator.c ++++ b/drivers/clk/qcom/gdsc-regulator.c +@@ -842,14 +842,17 @@ static int gdsc_get_resources(struct gdsc *sc, struct platform_device *pdev) + + sc->bus_handle = msm_bus_scale_register_client(sc->bus_pdata); + if (!sc->bus_handle) { +- dev_err(&pdev->dev, "Failed to register bus client\n"); + /* +- * msm_bus_scale_register_client() returns 0 for all +- * errors including when called before the bus driver +- * probes. Therefore, return -EPROBE_DEFER here so that +- * probing can be retried and this case handled. ++ * qkx: msm_bus does not reliably reprobe after kexec and ++ * deferred-probe retries are disabled in this diagnostic ++ * kernel. Returning -EPROBE_DEFER here permanently removes ++ * GDSCs such as mdss_core_gdsc, preventing SDE RSC and DRM ++ * from probing. All bus operations below already guard a ++ * zero handle, so retain the inherited interconnect state ++ * and continue without an explicit vote. + */ +- return -EPROBE_DEFER; ++ dev_warn(&pdev->dev, ++ "qkx: bus client unavailable; continuing without bus scaling\n"); + } + } + +diff --git a/drivers/cpufreq/cpufreq_stats.c b/drivers/cpufreq/cpufreq_stats.c +index 9ecc8a86f4..17e717d4a4 100644 +--- a/drivers/cpufreq/cpufreq_stats.c ++++ b/drivers/cpufreq/cpufreq_stats.c +@@ -299,38 +299,17 @@ void cpufreq_stats_record_transition(struct cpufreq_policy *policy, + stats->total_trans++; + } + ++/* ++ * QKX: the vendor per-cpu idle/busy time-in-state accounting races ++ * against cpufreq_stats_create_table() on secondary CPUs (policy stats are ++ * published before the tables are fully coherent), causing NULL derefs in ++ * the idle path. The accounting is pure diagnostics; disable it for the ++ * kexec diagnostic kernel. ++ */ + void cpufreq_stats_update_busy_time(struct task_struct *p, u64 cputime) + { +- struct cpufreq_policy *policy; +- unsigned long flags; +- unsigned int cpu; +- +- cpu = task_cpu(p); +- policy = cpufreq_cpu_get(cpu); +- if (policy) { +- if (policy->stats && !is_idle_task(p)) { +- spin_lock_irqsave(&cpufreq_stats_lock, flags); +- policy->stats->busy_time_in_state_per_cpu[cpu - policy->stats->cpu][policy->stats->last_index] += cputime; +- spin_unlock_irqrestore(&cpufreq_stats_lock, flags); +- } +- cpufreq_cpu_put(policy); +- } + } + + void cpufreq_stats_update_idle_time(u64 cputime) + { +- struct cpufreq_policy *policy; +- unsigned long flags; +- unsigned int cpu; +- +- cpu = smp_processor_id(); +- policy = cpufreq_cpu_get(cpu); +- if (policy) { +- if (policy->stats) { +- spin_lock_irqsave(&cpufreq_stats_lock, flags); +- policy->stats->idle_time_in_state_per_cpu[cpu - policy->stats->cpu][policy->stats->last_index] += cputime; +- spin_unlock_irqrestore(&cpufreq_stats_lock, flags); +- } +- cpufreq_cpu_put(policy); +- } + } +diff --git a/drivers/iommu/arm-smmu.c b/drivers/iommu/arm-smmu.c +index f23ac48009..651fd11e14 100644 +--- a/drivers/iommu/arm-smmu.c ++++ b/drivers/iommu/arm-smmu.c +@@ -5607,8 +5607,16 @@ static int arm_smmu_init_bus_scaling(struct arm_smmu_power_resources *pwr) + + pwr->bus_client = msm_bus_scale_register_client(pwr->bus_dt_data); + if (!pwr->bus_client) { +- dev_err(dev, "Bus client registration failed\n"); +- return -EPROBE_DEFER; ++ /* ++ * qkx: after kexec the msm_bus fabric driver may never come up ++ * (deferred retries are disabled in this diagnostic kernel), which ++ * would leave apps-smmu permanently dead and every SMMU client ++ * (e.g. dwc3 USB) running against stale translation. Bus voting is ++ * an optimization; continue without it. ++ */ ++ dev_warn(dev, ++ "qkx: bus client registration failed; continuing without bus scaling\n"); ++ return 0; + } + + return 0; +diff --git a/drivers/irqchip/irq-gic-v3.c b/drivers/irqchip/irq-gic-v3.c +index 7525167d47..074993ec62 100644 +--- a/drivers/irqchip/irq-gic-v3.c ++++ b/drivers/irqchip/irq-gic-v3.c +@@ -513,6 +513,25 @@ static void __init gic_dist_init(void) + for (i = 32; i < gic_data.irq_nr; i += 32) + writel_relaxed(~0, base + GICD_IGROUPR + i / 8); + ++ /* ++ * qkx: kexec hygiene. The previous kernel may have died with SPIs ++ * left pending or (worse) active without EOI. A GICv3 SPI stuck in ++ * active state is never re-signalled, permanently wedging the ++ * interrupt line (e.g. dwc3 serving ADB at kexec time). Clear both ++ * states for all SPIs while the distributor is disabled. ++ */ ++ for (i = 32; i < gic_data.irq_nr; i += 32) { ++ u32 act = readl_relaxed(base + GICD_ISACTIVER + i / 8); ++ u32 pend = readl_relaxed(base + GICD_ISPENDR + i / 8); ++ ++ if (act || pend) ++ pr_info("qkx: gic stale SPI state at kexec: hwirq %d-%d act=%08x pend=%08x\n", ++ i, min(i + 31, gic_data.irq_nr - 1), act, pend); ++ ++ writel_relaxed(~0, base + GICD_ICPENDR + i / 8); ++ writel_relaxed(~0, base + GICD_ICACTIVER + i / 8); ++ } ++ + gic_dist_config(base, gic_data.irq_nr, gic_dist_wait_for_rwp); + + /* Enable distributor with ARE, Group1 */ +diff --git a/drivers/mailbox/msm_qmp.c b/drivers/mailbox/msm_qmp.c +index 726f65677b..36a304d88c 100644 +--- a/drivers/mailbox/msm_qmp.c ++++ b/drivers/mailbox/msm_qmp.c +@@ -5,6 +5,7 @@ + + #include + #include ++#include + #include + #include + #include +@@ -33,6 +34,13 @@ + #define MSG_RAM_ALIGN_BYTES 3 + + #define QMP_IPC_LOG_PAGE_CNT 2 ++ ++/* Force a DOWN->UP QMP edge when inheriting live AOP state across kexec. */ ++static bool qkx_qmp_rehandshake; ++core_param(qkx_qmp_rehandshake, qkx_qmp_rehandshake, bool, 0444); ++static bool qkx_qmp_adopt; ++core_param(qkx_qmp_adopt, qkx_qmp_adopt, bool, 0444); ++bool qkx_qmp_inherited_ready; + #define QMP_INFO(ctxt, x, ...) \ + do { \ + if (ctxt) \ +@@ -162,6 +170,7 @@ struct qmp_mbox { + struct completion ch_complete; + struct delayed_work dwork; + struct qmp_device *mdev; ++ bool qkx_adopted; + }; + + /** +@@ -347,6 +356,20 @@ static int qmp_startup(struct mbox_chan *chan) + set_mcore_ch(mbox, QMP_MBOX_CH_CONNECTED); + mbox->local_state = LOCAL_CONNECTING; + send_irq(mbox->mdev); ++ if (mbox->qkx_adopted) { ++ struct mbox_desc desc; ++ ++ memcpy_fromio(&desc, mbox->desc, sizeof(desc)); ++ if (desc.ucore.ch_state == QMP_MBOX_CH_CONNECTED && ++ desc.ucore.ch_state_ack == QMP_MBOX_CH_CONNECTED && ++ desc.mcore.ch_state == QMP_MBOX_CH_CONNECTED && ++ desc.mcore.ch_state_ack == QMP_MBOX_CH_CONNECTED) { ++ mbox->local_state = CHANNEL_CONNECTED; ++ complete_all(&mbox->ch_complete); ++ pr_emerg("qkxqmp: %s adopted connected channel\n", ++ mbox->mdev->name); ++ } ++ } + } + mutex_unlock(&mbox->state_lock); + +@@ -510,6 +533,24 @@ static void init_mcore_state(struct qmp_mbox *mbox) + memcpy32_toio(mbox->desc + offset, &mcore, sizeof(mcore)); + } + ++static void qkx_reset_mcore_state(struct qmp_mbox *mbox) ++{ ++ struct channel_desc mcore = { ++ .link_state = QMP_MBOX_LINK_DOWN, ++ .link_state_ack = QMP_MBOX_LINK_DOWN, ++ .ch_state = QMP_MBOX_CH_DISCONNECTED, ++ .ch_state_ack = QMP_MBOX_CH_DISCONNECTED, ++ .mailbox_size = mbox->mcore_mbox_size, ++ .mailbox_offset = mbox->mcore_mbox_offset, ++ }; ++ u32 offset = offsetof(struct mbox_desc, mcore); ++ ++ memcpy32_toio(mbox->desc + offset, &mcore, sizeof(mcore)); ++ mbox->local_state = LINK_DISCONNECTED; ++ reinit_completion(&mbox->link_complete); ++ reinit_completion(&mbox->ch_complete); ++} ++ + /** + * qmp_irq_handler() - handle irq from remote entitity. + * @irq: irq number for the trggered interrupt. +@@ -568,7 +609,12 @@ static void __qmp_rx_worker(struct qmp_mbox *mbox) + case LINK_NEGOTIATION: + if (desc.mcore.link_state_ack != QMP_MBOX_LINK_UP || + desc.mcore.link_state != QMP_MBOX_LINK_UP) { +- QMP_ERR(mdev->ilc, "RX int without negotiation ack\n"); ++ QMP_ERR(mdev->ilc, ++ "RX without ack: ulink=%08x uack=%08x uch=%08x ucack=%08x mlink=%08x mack=%08x mch=%08x mcack=%08x\n", ++ desc.ucore.link_state, desc.ucore.link_state_ack, ++ desc.ucore.ch_state, desc.ucore.ch_state_ack, ++ desc.mcore.link_state, desc.mcore.link_state_ack, ++ desc.mcore.ch_state, desc.mcore.ch_state_ack); + break; + } + mbox->local_state = LINK_CONNECTED; +@@ -991,6 +1037,90 @@ static int qmp_mbox_probe(struct platform_device *pdev) + QMP_ERR(mdev->ilc, "enable_irq_wake on %d failed: %d\n", + mdev->rx_irq_line, ret); + ++ if (qkx_qmp_adopt) { ++ struct qmp_mbox *mbox; ++ ++ list_for_each_entry(mbox, &mdev->mboxes, list) { ++ struct mbox_desc desc; ++ unsigned int wait; ++ ++ memcpy_fromio(&desc, mbox->desc, sizeof(desc)); ++ if (desc.magic == QMP_MAGIC && ++ desc.ucore.link_state == QMP_MBOX_LINK_UP && ++ desc.ucore.link_state_ack == QMP_MBOX_LINK_UP && ++ desc.mcore.link_state == QMP_MBOX_LINK_UP && ++ desc.mcore.link_state_ack == QMP_MBOX_LINK_UP) { ++ /* Normal LINK_DISCONNECTED negotiation imports these ++ * values before publishing mcore state. Adoption bypasses ++ * that branch, so import them from the inherited descriptor. ++ */ ++ mbox->version.version = desc.version; ++ mbox->version.features = desc.features; ++ if (desc.mcore.mailbox_size) { ++ mbox->mcore_mbox_size = desc.mcore.mailbox_size; ++ mbox->mcore_mbox_offset = desc.mcore.mailbox_offset; ++ } ++ if (!mbox->rx_pkt.data && desc.ucore.mailbox_size) ++ mbox->rx_pkt.data = devm_kzalloc(mdev->dev, ++ desc.ucore.mailbox_size, GFP_KERNEL); ++ if (!mbox->rx_pkt.data) { ++ pr_emerg("qkxqmp: %s failed inherited RX allocation\n", ++ mdev->name); ++ continue; ++ } ++ /* Preserve the live AOP link, but close the inherited ++ * logical channel so the normal startup path creates a ++ * fresh channel transaction context for this kernel. ++ */ ++ disable_irq(mdev->rx_irq_line); ++ set_mcore_ch(mbox, QMP_MBOX_CH_DISCONNECTED); ++ /* Android may die with an outstanding nonzero mailbox ++ * length. AOP detects a new packet on zero -> nonzero; ++ * clear it while the inherited channel is closed. ++ */ ++ iowrite32(0, mbox->desc + mbox->mcore_mbox_offset); ++ mbox->tx_sent = false; ++ send_irq(mdev); ++ for (wait = 0; wait < 100; wait++) { ++ if (ioread32(mbox->desc + ++ offsetof(struct mbox_desc, ++ mcore.ch_state_ack)) == ++ QMP_MBOX_CH_DISCONNECTED) ++ break; ++ usleep_range(1000, 2000); ++ } ++ enable_irq(mdev->rx_irq_line); ++ if (wait == 100) { ++ pr_emerg("qkxqmp: %s inherited channel close timed out\n", ++ mdev->name); ++ continue; ++ } ++ mbox->local_state = LINK_CONNECTED; ++ mbox->qkx_adopted = false; ++ qkx_qmp_inherited_ready = true; ++ complete_all(&mbox->link_complete); ++ pr_emerg("qkxqmp: %s adopted link and closed inherited channel\n", ++ mdev->name); ++ } ++ } ++ } ++ ++ if (qkx_qmp_rehandshake) { ++ struct qmp_mbox *mbox; ++ ++ /* Android leaves the AP half LINK_UP. Writing UP again creates no ++ * edge for the still-running AOP. Keep RX masked so a stale/pending ++ * interrupt cannot make our worker overwrite DOWN before AOP sees it. ++ */ ++ disable_irq(mdev->rx_irq_line); ++ list_for_each_entry(mbox, &mdev->mboxes, list) ++ qkx_reset_mcore_state(mbox); ++ pr_emerg("qkxqmp: %s forced mcore link down\n", mdev->name); ++ send_irq(mdev); ++ msleep(20); ++ enable_irq(mdev->rx_irq_line); ++ } ++ + /* Trigger fake RX in case of missed interrupt */ + if (of_property_read_bool(edge_node, "qcom,early-boot")) + qmp_irq_handler(0, mdev); +diff --git a/drivers/misc/qseecom.c b/drivers/misc/qseecom.c +index 6b5d739a4d..e8819ccdc1 100644 +--- a/drivers/misc/qseecom.c ++++ b/drivers/misc/qseecom.c +@@ -1762,7 +1762,9 @@ static int __qseecom_unregister_listener_kthread_func(void *data) + wait_event_interruptible( + qseecom.unregister_lsnr_kthread_wq, + atomic_read(&qseecom.unregister_lsnr_kthread_state) +- == LSNR_UNREG_KT_WAKEUP); ++ == LSNR_UNREG_KT_WAKEUP || kthread_should_stop()); ++ if (kthread_should_stop()) ++ break; + pr_debug("kthread to unregister listener is called %d\n", + atomic_read(&qseecom.unregister_lsnr_kthread_state)); + __qseecom_processing_pending_lsnr_unregister(); +@@ -3268,7 +3270,9 @@ static int __qseecom_unload_app_kthread_func(void *data) + wait_event_interruptible( + qseecom.unload_app_kthread_wq, + atomic_read(&qseecom.unload_app_kthread_state) +- == UNLOAD_APP_KT_WAKEUP); ++ == UNLOAD_APP_KT_WAKEUP || kthread_should_stop()); ++ if (kthread_should_stop()) ++ break; + pr_debug("kthread to unload app is called, state %d\n", + atomic_read(&qseecom.unload_app_kthread_state)); + __qseecom_processing_pending_unload_app(); +diff --git a/drivers/of/device.c b/drivers/of/device.c +index 65235ff63c..c92512b688 100644 +--- a/drivers/of/device.c ++++ b/drivers/of/device.c +@@ -160,8 +160,21 @@ int of_dma_configure(struct device *dev, struct device_node *np, bool force_dma) + coherent ? " " : " not "); + + iommu = of_iommu_configure(dev, np); +- if (IS_ERR(iommu) && PTR_ERR(iommu) == -EPROBE_DEFER) +- return -EPROBE_DEFER; ++ if (IS_ERR(iommu) && PTR_ERR(iommu) == -EPROBE_DEFER) { ++ /* qkx: after kexec the apps-smmu never claims the platform bus ++ * (its probe defers permanently and deferred retries are ++ * disabled), which parks the dwc3-msm glue in dma_configure ++ * forever. The glue's iommu_map is unused and gadget DMA works ++ * through the sysdev with direct DMA/swiotlb, so continue ++ * without an iommu for this node only. */ ++ if (of_device_is_compatible(np, "qcom,dwc-usb3-msm")) { ++ dev_info(dev, ++ "qkx: iommu defer; continuing without iommu\n"); ++ iommu = NULL; ++ } else { ++ return -EPROBE_DEFER; ++ } ++ } + + dev_dbg(dev, "device is%sbehind an iommu\n", + iommu ? " " : " not "); +diff --git a/drivers/power/reset/msm-poweroff.c b/drivers/power/reset/msm-poweroff.c +index 13121cea46..0afe617363 100644 +--- a/drivers/power/reset/msm-poweroff.c ++++ b/drivers/power/reset/msm-poweroff.c +@@ -56,6 +56,8 @@ static bool scm_deassert_ps_hold_supported; + static void __iomem *msm_ps_hold; + static phys_addr_t tcsr_boot_misc_detect; + static void scm_disable_sdi(void); ++static void halt_spmi_pmic_arbiter(void); ++static void deassert_ps_hold(void); + + /* + * Runtime could be only changed value once. +@@ -72,8 +74,33 @@ static bool dload_mode_enabled; + static void *emergency_dload_mode_addr; + static bool scm_dload_supported; + ++/* Quest kexec diagnostics retain the reserved RAM log across checkpoints. */ + static bool force_warm_reboot; + ++void qkx_force_warm_reboot(void) ++{ ++ force_warm_reboot = true; ++} ++ ++/* ++ * Minimal, lock-free warm reset path. Safe to invoke from a secondary ++ * CPU while the boot CPU is hung inside an initcall with interrupts ++ * disabled: it touches no notifier lists, no device locks, and performs ++ * no cross-CPU synchronization. The PS_HOLD drop is a direct register ++ * write so the reset cannot be lost inside a wedged SCM call. ++ */ ++void qkx_minimal_warm_reset(void) ++{ ++ qpnp_pon_system_pwr_off(PON_POWER_OFF_WARM_RESET); ++ flush_cache_all(); ++ scm_disable_sdi(); ++ if (msm_ps_hold) ++ __raw_writel(0, msm_ps_hold); ++ for (;;) ++ mdelay(1000); ++} ++EXPORT_SYMBOL(qkx_minimal_warm_reset); ++ + /* interface for exporting attributes */ + struct reset_attribute { + struct attribute attr; +diff --git a/drivers/soc/qcom/msm_bus/msm_bus_fabric_rpmh.c b/drivers/soc/qcom/msm_bus/msm_bus_fabric_rpmh.c +index 3a48c03a8b..658464c5fc 100644 +--- a/drivers/soc/qcom/msm_bus/msm_bus_fabric_rpmh.c ++++ b/drivers/soc/qcom/msm_bus/msm_bus_fabric_rpmh.c +@@ -39,6 +39,45 @@ + ((vote_y & BCM_TCS_CMD_VOTE_MASK) << BCM_TCS_CMD_VOTE_Y_SHFT)) + + static int msm_bus_dev_init_qos(struct device *dev, void *data); ++ ++/* ++ * qkx: init_qos bisection. qkx_bus_qos_skip bits: 1 skip BCM votes, ++ * 2 skip QoS clocks, 4 skip QoS MMIO programming. qkx_bus_qos_max limits ++ * how many nodes get the full sequence (-1 = unlimited); later nodes are ++ * treated as bypass_qos_prg. ++ */ ++static int qkx_bus_qos_skip; ++core_param(qkx_bus_qos_skip, qkx_bus_qos_skip, int, 0444); ++static int qkx_bus_qos_max = -1; ++core_param(qkx_bus_qos_max, qkx_bus_qos_max, int, 0444); ++static int qkx_bus_qos_count; ++/* ++ * Comma-separated node ids whose QoS programming is skipped. ++ * ++ * Default 122 (mas-xm-ufs-card): Quest Pro has no UFS card. On a cold boot ++ * the bootloader leaves that aggre2_noc port clocked, so the early QoS write ++ * is harmless; after kexec the previous kernel's clk_disable_unused has ++ * gated it, and the unclocked NoC QoS write hard-resets the SoC (PMIC cold ++ * reset). Unlike usb3/ufs-mem, the DT gives this port no qos clock to enable ++ * first, so skip it. ++ */ ++static char *qkx_bus_qos_skipids = "122"; ++core_param(qkx_bus_qos_skipids, qkx_bus_qos_skipids, charp, 0444); ++ ++static bool qkx_bus_qos_skipped(int id) ++{ ++ const char *p = qkx_bus_qos_skipids; ++ int v; ++ ++ while (p && *p) { ++ if (sscanf(p, "%d", &v) == 1 && v == id) ++ return true; ++ p = strchr(p, ','); ++ if (p) ++ p++; ++ } ++ return false; ++} + static int msm_bus_dev_sbm_config(struct device *dev, bool enable); + + static struct list_head bcm_query_list_inorder[VCD_MAX_CNT]; +@@ -629,7 +668,9 @@ int msm_bus_commit_data(struct list_head *clist) + tcs_cmd_n_shrink(n_wake); + tcs_cmd_n_shrink(n_sleep); + ++ pr_emerg("qkxbus: commit rsc invalidate\n"); + ret = rpmh_invalidate(cur_mbox); ++ pr_emerg("qkxbus: commit invalidate ret=%d\n", ret); + if (ret) + MSM_BUS_ERR("%s: Error invalidating mbox: %d\n", + __func__, ret); +@@ -668,6 +709,8 @@ int msm_bus_commit_data(struct list_head *clist) + __func__, ret); + } + ++ pr_emerg("qkxbus: commit rsc=%d writes done ret=%d n=%d\n", ++ cur_rsc->node_info->id, ret, n_active[0]); + list_for_each_entry_safe(node, node_tmp, clist, link) { + if (unlikely(node->node_info->defer_qos)) + msm_bus_dev_init_qos(&node->dev, NULL); +@@ -988,9 +1031,26 @@ static int msm_bus_dev_init_qos(struct device *dev, void *data) + if (node_dev->ap_owned) { + if (bus_node_info->fabdev->bypass_qos_prg) + goto exit_init_qos; ++ if (qkx_bus_qos_max >= 0 && ++ qkx_bus_qos_count >= qkx_bus_qos_max) ++ goto exit_init_qos; ++ qkx_bus_qos_count++; ++ if (qkx_bus_qos_skipped(node_dev->node_info->id)) { ++ pr_emerg("qkxbus: qos node %d skipped\n", ++ node_dev->node_info->id); ++ goto exit_init_qos; ++ } + +- ret = msm_bus_vote_qos_bcms(node_dev); +- ret = msm_bus_enable_node_qos_clk(node_dev); ++ pr_emerg("qkxbus: qos #%d node %d vote\n", ++ qkx_bus_qos_count, ++ node_dev->node_info->id); ++ ret = 0; ++ if (!(qkx_bus_qos_skip & 1)) ++ ret = msm_bus_vote_qos_bcms(node_dev); ++ pr_emerg("qkxbus: qos node %d vote ret=%d clk\n", ++ node_dev->node_info->id, ret); ++ if (!(qkx_bus_qos_skip & 2)) ++ ret = msm_bus_enable_node_qos_clk(node_dev); + if (ret < 0) { + MSM_BUS_DBG("Can't Enable QoS clk %d\n", + node_dev->node_info->id); +@@ -998,14 +1058,24 @@ static int msm_bus_dev_init_qos(struct device *dev, void *data) + goto exit_init_qos; + } + +- bus_node_info->fabdev->noc_ops.qos_init( ++ pr_emerg("qkxbus: qos node %d mmio base=%pK off=%x qoff=%x\n", ++ node_dev->node_info->id, ++ bus_node_info->fabdev->qos_base, ++ bus_node_info->fabdev->base_offset, ++ bus_node_info->fabdev->qos_off); ++ if (!(qkx_bus_qos_skip & 4)) ++ bus_node_info->fabdev->noc_ops.qos_init( + node_dev, + bus_node_info->fabdev->qos_base, + bus_node_info->fabdev->base_offset, + bus_node_info->fabdev->qos_off, + bus_node_info->fabdev->qos_freq); +- ret = msm_bus_disable_node_qos_clk(node_dev); +- ret = msm_bus_rm_vote_qos_bcms(node_dev); ++ pr_emerg("qkxbus: qos node %d mmio done\n", ++ node_dev->node_info->id); ++ if (!(qkx_bus_qos_skip & 2)) ++ ret = msm_bus_disable_node_qos_clk(node_dev); ++ if (!(qkx_bus_qos_skip & 1)) ++ ret = msm_bus_rm_vote_qos_bcms(node_dev); + node_dev->node_info->defer_qos = false; + } + } else +@@ -1797,12 +1867,31 @@ int msm_bus_device_remove(struct platform_device *pdev) + return 0; + } + ++/* ++ * qkx: bisection aid for the post-kexec hard reset triggered by this probe. ++ * qkx_bus_stop=N makes the probe return early at phase N: ++ * 1 probe entry, 2 after node/clk init, 3 after dev_conn/postcon, ++ * 4 after init_qos (skips late_init commit); 0 runs everything. ++ */ ++static int qkx_bus_stop; ++core_param(qkx_bus_stop, qkx_bus_stop, int, 0444); ++static bool qkx_bus_probed; ++ ++#define QKX_BUS_STOP(n) do { \ ++ pr_emerg("qkxbus: phase %d reached\n", n); \ ++ if (qkx_bus_stop == (n)) { \ ++ pr_emerg("qkxbus: stopping at phase %d\n", n); \ ++ return -ENODEV; \ ++ } \ ++} while (0) ++ + static int msm_bus_device_probe(struct platform_device *pdev) + { + unsigned int i = 1, ret; + struct msm_bus_device_node_registration *pdata; + + MSM_BUS_ERR("msm_bus: Probe started"); ++ QKX_BUS_STOP(1); + /* If possible, get pdata from device-tree */ + if (pdev->dev.of_node) + pdata = msm_bus_of_to_pdata(pdev); +@@ -1866,6 +1955,8 @@ static int msm_bus_device_probe(struct platform_device *pdev) + } + } + ++ QKX_BUS_STOP(2); ++ pr_emerg("qkxbus: phase dev_conn (%d nodes)\n", pdata->num_devices); + ret = bus_for_each_dev(&msm_bus_type, NULL, NULL, + msm_bus_setup_dev_conn); + if (ret) { +@@ -1873,6 +1964,7 @@ static int msm_bus_device_probe(struct platform_device *pdev) + goto exit_device_probe; + } + ++ pr_emerg("qkxbus: phase postcon\n"); + ret = bus_for_each_dev(&msm_bus_type, NULL, NULL, + msm_bus_postcon_setup); + if (ret) { +@@ -1885,7 +1977,13 @@ static int msm_bus_device_probe(struct platform_device *pdev) + * defer QoS programming to the first transaction in cases of failure + * and we want to continue the probe. + */ ++ QKX_BUS_STOP(3); ++ pr_emerg("qkxbus: phase init_qos\n"); + ret = bus_for_each_dev(&msm_bus_type, NULL, NULL, msm_bus_dev_init_qos); ++ pr_emerg("qkxbus: phase init_qos done (%d nodes programmed)\n", ++ qkx_bus_qos_count); ++ QKX_BUS_STOP(4); ++ qkx_bus_probed = true; + + /* Register the arb layer ops */ + msm_bus_arb_setops_adhoc(&arb_ops); +@@ -2035,6 +2133,11 @@ int __init msm_bus_device_init_driver(void) + + int __init msm_bus_device_late_init(void) + { ++ if (!qkx_bus_probed) { ++ pr_emerg("qkxbus: probe incomplete; skipping late_init commit\n"); ++ return 0; ++ } ++ pr_emerg("qkxbus: late_init commit\n"); + commit_late_init_data(true); + MSM_BUS_ERR("msm_bus_late_init: Remove handoff bw requests\n"); + init_time = false; +diff --git a/drivers/soc/qcom/msm_bus/msm_bus_proxy_client.c b/drivers/soc/qcom/msm_bus/msm_bus_proxy_client.c +index d3ca18fcbe..56de67fcb6 100644 +--- a/drivers/soc/qcom/msm_bus/msm_bus_proxy_client.c ++++ b/drivers/soc/qcom/msm_bus/msm_bus_proxy_client.c +@@ -81,5 +81,9 @@ static int __init msm_bus_proxy_client_unvote(void) + return 0; + } + +-subsys_initcall_sync(msm_bus_proxy_client_init_driver); +-late_initcall_sync(msm_bus_proxy_client_unvote); ++/* Quest kexec: the RPMh bus fabric is deliberately disabled, so registering ++ * its proxy client would block forever. The diagnostic environment does not ++ * need bus bandwidth votes. ++ */ ++/* subsys_initcall_sync(msm_bus_proxy_client_init_driver); */ ++/* late_initcall_sync(msm_bus_proxy_client_unvote); */ +diff --git a/drivers/usb/dwc3/dwc3-msm.c b/drivers/usb/dwc3/dwc3-msm.c +index fb9eafcb8b..ea90f09454 100644 +--- a/drivers/usb/dwc3/dwc3-msm.c ++++ b/drivers/usb/dwc3/dwc3-msm.c +@@ -3716,6 +3716,7 @@ static int dwc_dpdm_cb(struct notifier_block *nb, unsigned long evt, void *p) + static int dwc3_msm_probe(struct platform_device *pdev) + { + struct device_node *node = pdev->dev.of_node, *dwc3_node; ++ struct device_node *child = NULL; + struct device *dev = &pdev->dev; + struct dwc3_msm *mdwc; + struct dwc3 *dwc; +@@ -3727,6 +3728,8 @@ static int dwc3_msm_probe(struct platform_device *pdev) + if (!mdwc) + return -ENOMEM; + ++ dev_info(&pdev->dev, "qkx: dwc3_msm_probe enter\n"); ++ + platform_set_drvdata(pdev, mdwc); + mdwc->dev = &pdev->dev; + +@@ -3759,7 +3762,7 @@ static int dwc3_msm_probe(struct platform_device *pdev) + /* Get all clks and gdsc reference */ + ret = dwc3_msm_get_clk_gdsc(mdwc); + if (ret) { +- dev_err(&pdev->dev, "error getting clock or gdsc.\n"); ++ dev_err(&pdev->dev, "error getting clock or gdsc. ret=%d\n", ret); + goto err; + } + +@@ -3916,8 +3919,16 @@ static int dwc3_msm_probe(struct platform_device *pdev) + } + } + +- /* Assumes dwc3 is the first DT child of dwc3-msm */ +- dwc3_node = of_get_next_available_child(node, NULL); ++ /* Assumes dwc3 is the first DT child of dwc3-msm. qkx: the captured ++ * runtime DTB lists qcom,usbbam before dwc3, so look the dwc3 child ++ * up by compatible instead of blindly taking the first child. */ ++ dwc3_node = NULL; ++ for_each_available_child_of_node(node, child) { ++ if (of_device_is_compatible(child, "snps,dwc3")) { ++ dwc3_node = child; ++ break; ++ } ++ } + if (!dwc3_node) { + dev_err(&pdev->dev, "failed to find dwc3 child\n"); + ret = -ENODEV; +@@ -4027,7 +4038,16 @@ static int dwc3_msm_probe(struct platform_device *pdev) + + if (of_property_read_bool(node, "extcon")) { + ret = dwc3_msm_extcon_register(mdwc); +- if (ret) ++ if (ret == -EPROBE_DEFER) { ++ /* qkx: the pdphy extcon provider never comes up after ++ * kexec, so deferring here parks USB forever (deferred ++ * retries are disabled in the diagnostic kernel). Mirror ++ * the no-extcon DRD default and start peripheral mode. */ ++ dev_info(mdwc->dev, ++ "qkx: extcon defer; starting peripheral mode\n"); ++ mdwc->vbus_active = true; ++ dwc3_ext_event_notify(mdwc); ++ } else if (ret) + goto put_dwc3; + + /* +@@ -4984,7 +5004,10 @@ MODULE_DESCRIPTION("DesignWare USB3 MSM Glue Layer"); + + static int dwc3_msm_init(void) + { +- return platform_driver_register(&dwc3_msm_driver); ++ int ret = platform_driver_register(&dwc3_msm_driver); ++ ++ pr_info("qkx: dwc3_msm_init platform_driver_register=%d\n", ret); ++ return ret; + } + module_init(dwc3_msm_init); + +diff --git a/drivers/usb/dwc3/gadget.c b/drivers/usb/dwc3/gadget.c +index fb1bdcd358..cffabd63bc 100644 +--- a/drivers/usb/dwc3/gadget.c ++++ b/drivers/usb/dwc3/gadget.c +@@ -22,6 +22,7 @@ + #include + #include + #include ++#include + + #include "debug.h" + #include "core.h" +@@ -2493,12 +2494,18 @@ static int dwc3_gadget_pullup(struct usb_gadget *g, int is_on) + * the udc_start. + */ + spin_unlock_irqrestore(&dwc->lock, flags); ++ dev_info(dwc->dev, ++ "qkx: pullup(%d) deferred: vbus_active=%d driver=%d err=%d\n", ++ is_on, dwc->vbus_active, !!dwc->gadget_driver, ++ dwc->err_evt_seen); + dbg_event(0xFF, "WaitPullup", 0); + return 0; + } + spin_unlock_irqrestore(&dwc->lock, flags); + ++ dev_info(dwc->dev, "qkx: pullup(%d) proceeding\n", is_on); + pm_runtime_get_sync(dwc->dev); ++ dev_info(dwc->dev, "qkx: pullup pm_runtime_get_sync done\n"); + dbg_event(0xFF, "Pullup gsync", + atomic_read(&dwc->dev->power.usage_count)); + +@@ -2553,6 +2560,8 @@ static int dwc3_gadget_pullup(struct usb_gadget *g, int is_on) + + ret = dwc3_gadget_run_stop_util(dwc); + spin_unlock_irqrestore(&dwc->lock, flags); ++ dev_info(dwc->dev, "qkx: pullup run_stop_util ret=%d gadget_state=%d\n", ++ ret, dwc->gadget_state); + if (!is_on && ret == -ETIMEDOUT) { + /* + * If we fail to stop the controller then mark it as an error +@@ -2574,6 +2583,67 @@ static int dwc3_gadget_pullup(struct usb_gadget *g, int is_on) + return ret; + } + ++/* qkx: post-bind USB state dump — decodes whether the core ever saw the ++ * host's bus reset (DSTS DCNN/connect + SSTS speed) and whether the event ++ * machinery produced anything (GEVNTCOUNT), plus GIC line state via the ++ * irqchip API (no raw MMIO). ++ */ ++static void qkx_usb_dump_regs(struct dwc3 *dwc, const char *tag) ++{ ++ bool pend = false, act = false; ++ struct device *p = dwc->dev->parent; ++ bool susp = pm_runtime_suspended(dwc->dev); ++ bool psusp = p ? pm_runtime_suspended(p) : false; ++ ++ irq_get_irqchip_state(dwc->irq, IRQCHIP_STATE_PENDING, &pend); ++ irq_get_irqchip_state(dwc->irq, IRQCHIP_STATE_ACTIVE, &act); ++ dev_info(dwc->dev, ++ "qkx: %s: pm(core susp=%d use=%d parent susp=%d use=%d) in_lpm=%d gic(pend=%d act=%d)\n", ++ tag, susp, ++ atomic_read(&dwc->dev->power.usage_count), psusp, ++ p ? atomic_read(&p->power.usage_count) : -1, ++ atomic_read(&dwc->in_lpm), pend, act); ++ ++ /* read-only: no pm_runtime_get_sync here — resuming would CSFTRST and ++ * re-init the core, perturbing the very state under observation. */ ++ if (!susp && !psusp) { ++ u32 dctl, dsts, devten, evsiz, evcnt; ++ ++ dctl = dwc3_readl(dwc->regs, DWC3_DCTL); ++ dsts = dwc3_readl(dwc->regs, DWC3_DSTS); ++ devten = dwc3_readl(dwc->regs, DWC3_DEVTEN); ++ evsiz = dwc3_readl(dwc->regs, DWC3_GEVNTSIZ(0)); ++ evcnt = dwc3_readl(dwc->regs, DWC3_GEVNTCOUNT(0)); ++ dev_info(dwc->dev, ++ "qkx: %s: GSNPSID=%08x GCTL=%08x DCTL=%08x DSTS=%08x (spd=%d link=%d) DEVTEN=%08x EVSIZ=%08x EVCNT=%08x\n", ++ tag, dwc3_readl(dwc->regs, DWC3_GSNPSID), ++ dwc3_readl(dwc->regs, DWC3_GCTL), dctl, dsts, ++ dsts & DWC3_DSTS_CONNECTSPD, ++ (dsts & DWC3_DSTS_USBLNKST_MASK) >> 18, devten, ++ evsiz, evcnt); ++ } else { ++ dev_info(dwc->dev, "qkx: %s: skipping MMIO reads (runtime suspended)\n", ++ tag); ++ } ++} ++ ++static void qkx_usb_dump_work(struct work_struct *w); ++static DECLARE_DELAYED_WORK(qkx_usb_dump_work_w, qkx_usb_dump_work); ++static struct dwc3 *qkx_dump_dwc; ++static int qkx_dump_rounds; ++ ++static void qkx_usb_dump_work(struct work_struct *w) ++{ ++ struct dwc3 *dwc = qkx_dump_dwc; ++ ++ if (!dwc) ++ return; ++ qkx_usb_dump_regs(dwc, "post-bind dump"); ++ if (++qkx_dump_rounds < 4) ++ schedule_delayed_work(&qkx_usb_dump_work_w, ++ msecs_to_jiffies(3000)); ++} ++ + static void dwc3_gadget_enable_irq(struct dwc3 *dwc) + { + u32 reg; +@@ -2668,6 +2738,9 @@ static int dwc3_gadget_vbus_session(struct usb_gadget *_gadget, int is_active) + + dbg_event(0xFF, "VbusSess", is_active); + ++ dev_info(dwc->dev, "qkx: vbus_session(%d) enter vbus_active=%d state=%d\n", ++ is_active, dwc->vbus_active, dwc->gadget_state); ++ + disable_irq(dwc->irq); + + flush_work(&dwc->bh_work); +@@ -2774,6 +2847,21 @@ static int __dwc3_gadget_start(struct dwc3 *dwc) + + dwc3_gadget_enable_irq(dwc); + ++ dev_info(dwc->dev, ++ "qkx: gadget_start done: DEVTEN=%08x irq=%d hwirq=untouched\n", ++ dwc3_readl(dwc->regs, DWC3_DEVTEN), dwc->irq); ++ ++ /* qkx: pin the dwc3 core child awake while the gadget driver is ++ * bound. Runtime-PM autosuspend parks the core ~1s after bind, ++ * killing the gadget mid-enumeration (host sees chirp then -110). */ ++ pm_runtime_get_noresume(dwc->dev); ++ dev_info(dwc->dev, "qkx: core pinned awake (use=%d)\n", ++ atomic_read(&dwc->dev->power.usage_count)); ++ ++ qkx_dump_dwc = dwc; ++ qkx_dump_rounds = 0; ++ schedule_delayed_work(&qkx_usb_dump_work_w, msecs_to_jiffies(2000)); ++ + return 0; + + err1: +@@ -2834,6 +2922,9 @@ static int dwc3_gadget_stop(struct usb_gadget *g) + dwc->gadget_driver = NULL; + spin_unlock_irqrestore(&dwc->lock, flags); + ++ /* qkx: release the gadget_start pin */ ++ pm_runtime_put_noidle(dwc->dev); ++ + dbg_event(0xFF, "fwq_started", 0); + flush_workqueue(dwc->dwc_wq); + dbg_event(0xFF, "fwq_completed", 0); +diff --git a/drivers/usb/phy/phy-msm-ssusb-qmp.c b/drivers/usb/phy/phy-msm-ssusb-qmp.c +index 33a47180c2..2d0ed6bda6 100644 +--- a/drivers/usb/phy/phy-msm-ssusb-qmp.c ++++ b/drivers/usb/phy/phy-msm-ssusb-qmp.c +@@ -784,6 +784,14 @@ static int msm_ssphy_qmp_extcon_register(struct msm_ssphy_qmp *phy, + + edev = extcon_get_edev_by_phandle(dev, 0); + if (IS_ERR(edev)) { ++ /* qkx: the pdphy extcon provider never registers after kexec ++ * (its usb/dc power supplies stay absent). Continue without the ++ * DP extcon so the PHY still binds; matches the non-DP PHY state. */ ++ if (PTR_ERR(edev) == -EPROBE_DEFER) { ++ dev_info(dev, ++ "qkx: extcon deferring; continuing without extcon\n"); ++ return 0; ++ } + dev_err(dev, "failed to get phandle for msm_ssphy_qmp\n"); + return PTR_ERR(edev); + } +diff --git a/drivers/usb/phy/phy.c b/drivers/usb/phy/phy.c +index 25b20539e0..6054c8af92 100644 +--- a/drivers/usb/phy/phy.c ++++ b/drivers/usb/phy/phy.c +@@ -53,8 +53,10 @@ static struct usb_phy *__of_usb_find_phy(struct device_node *node) + { + struct usb_phy *phy; + +- if (!of_device_is_available(node)) ++ if (!of_device_is_available(node)) { ++ pr_info("qkx: find_phy: node %pOF not available\n", node); + return ERR_PTR(-ENODEV); ++ } + + list_for_each_entry(phy, &phy_list, head) { + if (node != phy->dev->of_node) +@@ -63,6 +65,10 @@ static struct usb_phy *__of_usb_find_phy(struct device_node *node) + return phy; + } + ++ list_for_each_entry(phy, &phy_list, head) ++ pr_info("qkx: find_phy: want %pOF; list has %s node %pOF\n", ++ node, dev_name(phy->dev), phy->dev->of_node); ++ + return ERR_PTR(-EPROBE_DEFER); + } + +@@ -318,8 +324,17 @@ static int usb_add_extcon(struct usb_phy *x) + + if (of_property_read_bool(x->dev->of_node, "extcon")) { + x->edev = extcon_get_edev_by_phandle(x->dev, 0); +- if (IS_ERR(x->edev)) ++ if (IS_ERR(x->edev)) { ++ /* qkx: after kexec the pdphy extcon provider never ++ * appears; register the PHY without extcon support. */ ++ if (PTR_ERR(x->edev) == -EPROBE_DEFER) { ++ dev_info(x->dev, ++ "qkx: extcon defer; phy without extcon\n"); ++ x->edev = NULL; ++ goto no_extcon; ++ } + return PTR_ERR(x->edev); ++ } + + x->id_edev = extcon_get_edev_by_phandle(x->dev, 1); + if (IS_ERR(x->id_edev)) { +@@ -395,6 +410,7 @@ static int usb_add_extcon(struct usb_phy *x) + } + } + ++no_extcon: + if (x->type_nb.notifier_call) + __usb_phy_get_charger_type(x); + +@@ -555,6 +571,8 @@ struct usb_phy *devm_usb_get_phy_by_phandle(struct device *dev, + + node = of_parse_phandle(dev->of_node, phandle, index); + if (!node) { ++ dev_info(dev, "qkx: get_phy: phandle %s idx %u missing in %pOF\n", ++ phandle, index, dev->of_node); + dev_dbg(dev, "failed to get %s phandle in %pOF node\n", phandle, + dev->of_node); + return ERR_PTR(-ENODEV); +diff --git a/drivers/watchdog/watchdog_core.c b/drivers/watchdog/watchdog_core.c +index 5c600a3706..e5f6bedbce 100644 +--- a/drivers/watchdog/watchdog_core.c ++++ b/drivers/watchdog/watchdog_core.c +@@ -405,7 +405,10 @@ static void __exit watchdog_exit(void) + ida_destroy(&watchdog_ida); + } + +-subsys_initcall_sync(watchdog_init); ++/* Quest kexec RAM diagnostic: registering inherited watchdog devices can ++ * block in hardware-specific setup. No watchdog userspace is required. ++ */ ++/* subsys_initcall_sync(watchdog_init); */ + module_exit(watchdog_exit); + + MODULE_AUTHOR("Alan Cox "); +diff --git a/init/main.c b/init/main.c +index c1b2e6c6cd..db071cfa5d 100644 +--- a/init/main.c ++++ b/init/main.c +@@ -91,6 +91,7 @@ + #include + #include + #include ++#include + + #include + #include +@@ -552,6 +553,70 @@ static void __init mm_init(void) + pti_init(); + } + ++#define QKX_LOG_PHYS 0x9ba80000ULL ++#define QKX_LOG_SIZE 0x10000 ++#define QKX_LOG_MAGIC 0x514b584c ++ ++struct qkx_log_header { ++ u32 magic; ++ u32 write_pos; ++ u32 wraps; ++ u32 reserved; ++}; ++ ++static void __iomem *qkx_log; ++ ++static void qkx_log_write(struct console *con, const char *s, unsigned int count) ++{ ++ struct qkx_log_header __iomem *h = qkx_log; ++ u32 pos, wraps; ++ unsigned int i; ++ const u32 capacity = QKX_LOG_SIZE - sizeof(*h); ++ ++ if (!h || readl_relaxed(&h->magic) != QKX_LOG_MAGIC) ++ return; ++ pos = readl_relaxed(&h->write_pos); ++ wraps = readl_relaxed(&h->wraps); ++ for (i = 0; i < count; i++) { ++ writeb_relaxed(s[i], qkx_log + sizeof(*h) + pos); ++ if (++pos == capacity) { ++ pos = 0; ++ wraps++; ++ } ++ } ++ writel_relaxed(pos, &h->write_pos); ++ writel_relaxed(wraps, &h->wraps); ++ wmb(); ++} ++ ++static struct console qkx_log_console = { ++ .name = "qkxram", ++ .write = qkx_log_write, ++ .flags = CON_ENABLED | CON_PRINTBUFFER, ++ .index = -1, ++}; ++ ++static void __init qkx_log_console_init(void) ++{ ++ struct qkx_log_header __iomem *h; ++ struct page *pages[QKX_LOG_SIZE / PAGE_SIZE]; ++ unsigned int i; ++ ++ for (i = 0; i < ARRAY_SIZE(pages); i++) ++ pages[i] = pfn_to_page((QKX_LOG_PHYS >> PAGE_SHIFT) + i); ++ qkx_log = vmap(pages, ARRAY_SIZE(pages), VM_MAP | VM_IOREMAP, ++ pgprot_writecombine(PAGE_KERNEL)); ++ if (!qkx_log) ++ return; ++ h = qkx_log; ++ memset_io(qkx_log, 0, QKX_LOG_SIZE); ++ writel_relaxed(0, &h->write_pos); ++ writel_relaxed(0, &h->wraps); ++ writel_relaxed(QKX_LOG_MAGIC, &h->magic); ++ wmb(); ++ register_console(&qkx_log_console); ++} ++ + asmlinkage __visible void __init start_kernel(void) + { + char *command_line; +@@ -686,6 +751,7 @@ asmlinkage __visible void __init start_kernel(void) + * this. But we do want output early, in case something goes wrong. + */ + console_init(); ++ qkx_log_console_init(); + if (panic_later) + panic("Too many boot %s vars at `%s'", panic_later, + panic_param); +@@ -959,9 +1025,227 @@ static char *initcall_level_names[] __initdata = { + "late", + }; + ++static void __iomem *qkx_wdt; ++ ++static void __init qkx_force_watchdog_off(void) ++{ ++ if (!qkx_wdt) ++ qkx_wdt = ioremap(0x17c10000, 0x1000); ++ if (qkx_wdt) { ++ writel_relaxed(0, qkx_wdt + 0x08); ++ mb(); ++ } ++} ++ ++static atomic_t qkx_initcall_progress = ATOMIC_INIT(0); ++static atomic_t qkx_cur_level = ATOMIC_INIT(-1); ++static atomic_t qkx_cur_index = ATOMIC_INIT(-1); ++static atomic_t qkx_boot_done = ATOMIC_INIT(0); ++static char qkx_cur_name[128]; ++ ++extern void qkx_force_warm_reboot(void); ++extern void qkx_minimal_warm_reset(void) __noreturn; ++ ++static int qkx_panic_warm_reset(struct notifier_block *nb, ++ unsigned long event, void *ptr) ++{ ++ /* Avoid the Qualcomm kernel_panic restart reason: Android's ++ * panicreporter loads rdbg and can itself panic before retained RAM is ++ * collected. qpnp_pon and msm-poweroff are ready by the initcall stages ++ * where diagnostic target panics have occurred. */ ++ qkx_minimal_warm_reset(); ++ return NOTIFY_DONE; ++} ++ ++static struct notifier_block qkx_panic_nb = { ++ .notifier_call = qkx_panic_warm_reset, ++ .priority = INT_MAX, ++}; ++ ++static void qkx_note_phase(int level, int index, const char *fmt, ...) ++{ ++ va_list ap; ++ ++ qkx_cur_name[0] = '\0'; ++ va_start(ap, fmt); ++ vscnprintf(qkx_cur_name, sizeof(qkx_cur_name), fmt, ap); ++ va_end(ap); ++ /* publish name before the coordinates that reference it */ ++ smp_wmb(); ++ atomic_set(&qkx_cur_level, level); ++ atomic_set(&qkx_cur_index, index); ++ atomic_inc(&qkx_initcall_progress); ++} ++ ++static void __init __maybe_unused qkx_diag_warm_reset(void) ++{ ++ pr_emerg("QKX: deliberate retained-log warm reset\n"); ++ qkx_force_warm_reboot(); ++ machine_restart("qkx-log"); ++ panic("QKX: machine_restart returned"); ++} ++ ++/* ++ * Boot watchdog running on a secondary CPU. If boot progress ++ * stalls for several seconds (boot CPU hung with interrupts off), ++ * record the stall location directly in the retained RAM log and ++ * force a DRAM-preserving warm reset so the log can be recovered ++ * after Android returns. ++ */ ++static int qkx_boot_watchdog(void *unused) ++{ ++ u64 start_ns = ktime_get_mono_fast_ns(); ++ u64 last_progress_ns = start_ns; ++ u64 last_hb_ns = start_ns; ++ int progress = atomic_read(&qkx_initcall_progress); ++ ++ /* Pure busy loop: never sleep, never rely on timer IRQs or on ++ * wakeups from deep idle (whose RPMh/AOP-assisted wake path may ++ * be dead after the kexec handoff). ktime_get_mono_fast_ns() ++ * reads the arch counter directly, so this works with all ++ * interrupts disabled and the scheduler frozen. */ ++ while (!atomic_read(&qkx_boot_done)) { ++ int cur = atomic_read(&qkx_initcall_progress); ++ u64 now = ktime_get_mono_fast_ns(); ++ u64 stall_ms, total_ms, limit_ms; ++ char msg[192]; ++ int len; ++ ++ if (cur != progress) { ++ progress = cur; ++ last_progress_ns = now; ++ } ++ ++ stall_ms = (now - last_progress_ns) / 1000000; ++ total_ms = (now - start_ns) / 1000000; ++ ++ /* Heartbeat into the retained log every 5s. */ ++ if (now - last_hb_ns >= 5000000000ULL) { ++ last_hb_ns = now; ++ smp_rmb(); ++ len = scnprintf(msg, sizeof(msg), ++ "QKX: hb t=%llums level=%d idx=%d fn %s\n", ++ (unsigned long long)total_ms, ++ atomic_read(&qkx_cur_level), ++ atomic_read(&qkx_cur_index), ++ qkx_cur_name); ++ qkx_log_write(NULL, msg, len); ++ } ++ ++ /* Allow the built-in ~21s RCU stall detector to identify any CPU ++ * blocking an expedited grace period before recovery resets us. */ ++ limit_ms = (atomic_read(&qkx_cur_level) == 99) ? ++ 60000 : 30000; ++ ++ /* Total boot time cap: kernel must reach userspace ++ * (level 99) within 240s even with slow probes. */ ++ if (atomic_read(&qkx_cur_level) != 99 && total_ms > 240000) { ++ smp_rmb(); ++ len = scnprintf(msg, sizeof(msg), ++ "QKX: boot watchdog: 240s boot cap hit at level %d idx %d fn %s; forcing DRAM-preserving warm reset\n", ++ atomic_read(&qkx_cur_level), ++ atomic_read(&qkx_cur_index), ++ qkx_cur_name); ++ qkx_log_write(NULL, msg, len); ++ qkx_minimal_warm_reset(); ++ } ++ ++ if (stall_ms >= limit_ms) { ++ smp_rmb(); ++ len = scnprintf(msg, sizeof(msg), ++ "QKX: boot watchdog: no progress for %ums; stalled at level %d index %d fn %s; forcing DRAM-preserving warm reset\n", ++ (unsigned int)stall_ms, ++ atomic_read(&qkx_cur_level), ++ atomic_read(&qkx_cur_index), qkx_cur_name); ++ qkx_log_write(NULL, msg, len); ++ /* Capture PID 1's live stack before resetting. This is more ++ * useful than another layer of initcall markers when a shared ++ * synchronization primitive stalls at varying call sites. */ ++ { ++ struct task_struct *init_task = NULL; ++ ++ rcu_read_lock(); ++ init_task = find_task_by_vpid(1); ++ if (init_task) ++ get_task_struct(init_task); ++ rcu_read_unlock(); ++ if (init_task) { ++ pr_emerg("QKX: watchdog PID 1 stack follows\n"); ++ sched_show_task(init_task); ++ put_task_struct(init_task); ++ mdelay(500); ++ } ++ } ++ qkx_minimal_warm_reset(); ++ } ++ ++ mdelay(20); ++ /* The watchdog intentionally never sleeps, but a permanent busy ++ * loop otherwise prevents this CPU from reporting an RCU ++ * quiescent state and deadlocks synchronize_rcu() during boot. */ ++ rcu_note_voluntary_context_switch(current); ++ } ++ return 0; ++} ++ ++static void __init qkx_start_boot_watchdog(void) ++{ ++ struct task_struct *t; ++ int cpu = 1; ++ ++ atomic_notifier_chain_register(&panic_notifier_list, &qkx_panic_nb); ++ ++ while (cpu < num_possible_cpus() && !cpu_online(cpu)) ++ cpu++; ++ if (cpu >= num_possible_cpus()) { ++ pr_warn("QKX: no secondary CPU for boot watchdog\n"); ++ return; ++ } ++ t = kthread_create(qkx_boot_watchdog, NULL, "qkx-bwdog"); ++ if (IS_ERR(t)) { ++ pr_warn("QKX: failed to create boot watchdog\n"); ++ return; ++ } ++ kthread_bind(t, cpu); ++ wake_up_process(t); ++ pr_info("QKX: boot watchdog pinned to cpu%d\n", cpu); ++} ++ ++/* ++ * Userspace keepalive channel: initramfs /init writes 'd' to disarm the ++ * boot watchdog once it reaches its steady state, or anything else to ++ * refresh progress while it is deliberately slow. ++ */ ++void qkx_deferred_probe_kick(void); ++ ++static ssize_t qkx_bootdone_write(struct file *f, const char __user *buf, ++ size_t n, loff_t *pos) ++{ ++ char c; ++ ++ if (n < 1 || copy_from_user(&c, buf, 1)) ++ return -EINVAL; ++ if (c == 'd') ++ atomic_set(&qkx_boot_done, 1); ++ else if (c == 'p') ++ qkx_deferred_probe_kick(); ++ else if (c == 'r') { ++ pr_emerg("QKX: userspace requested DRAM-preserving warm reset\n"); ++ qkx_minimal_warm_reset(); ++ } else ++ atomic_inc(&qkx_initcall_progress); ++ return n; ++} ++ ++static const struct file_operations qkx_bootdone_fops = { ++ .owner = THIS_MODULE, ++ .write = qkx_bootdone_write, ++}; ++ + static void __init do_initcall_level(int level) + { + initcall_entry_t *fn; ++ unsigned int qkx_index = 0; + + strcpy(initcall_command_line, saved_command_line); + parse_args(initcall_level_names[level], +@@ -971,8 +1255,14 @@ static void __init do_initcall_level(int level) + NULL, &repair_env_string); + + trace_initcall_level(initcall_level_names[level]); +- for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) ++ for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) { ++ qkx_force_watchdog_off(); ++ qkx_note_phase(level, qkx_index, "%pf", initcall_from_entry(fn)); + do_one_initcall(initcall_from_entry(fn)); ++ atomic_inc(&qkx_initcall_progress); ++ qkx_force_watchdog_off(); ++ qkx_index++; ++ } + } + + static void __init do_initcalls(void) +@@ -992,12 +1282,18 @@ static void __init do_initcalls(void) + */ + static void __init do_basic_setup(void) + { ++ qkx_note_phase(-1, 0, "cpuset_init_smp"); + cpuset_init_smp(); ++ qkx_note_phase(-1, 1, "shmem_init"); + shmem_init(); ++ qkx_note_phase(-1, 2, "driver_init"); + driver_init(); ++ qkx_note_phase(-1, 3, "init_irq_proc"); + init_irq_proc(); ++ qkx_note_phase(-1, 4, "do_ctors"); + do_ctors(); + usermodehelper_enable(); ++ qkx_note_phase(-1, 5, "do_initcalls"); + do_initcalls(); + } + +@@ -1141,6 +1437,8 @@ static noinline void __init kernel_init_freeable(void) + */ + wait_for_completion(&kthreadd_done); + ++ qkx_note_phase(-1, 100, "kthreadd_done"); ++ + /* Now the scheduler is fully set up and can do blocking allocations */ + gfp_allowed_mask = __GFP_BITS_MASK; + +@@ -1153,23 +1451,31 @@ static noinline void __init kernel_init_freeable(void) + + smp_prepare_cpus(setup_max_cpus); + ++ qkx_note_phase(-1, 101, "workqueue_init"); + workqueue_init(); +- ++ qkx_note_phase(-1, 102, "init_mm_internals"); + init_mm_internals(); +- ++ qkx_note_phase(-1, 103, "do_pre_smp_initcalls"); + do_pre_smp_initcalls(); ++ qkx_note_phase(-1, 104, "lockup_detector_init"); + lockup_detector_init(); +- ++ qkx_note_phase(-1, 105, "smp_init"); + smp_init(); ++ qkx_note_phase(-1, 106, "sched_init_smp"); + sched_init_smp(); ++ qkx_start_boot_watchdog(); + ++ qkx_note_phase(-1, 107, "page_alloc_init_late"); + page_alloc_init_late(); + /* Initialize page ext after all struct pages are initialized. */ ++ qkx_note_phase(-1, 108, "page_ext_init"); + page_ext_init(); + ++ qkx_note_phase(-1, 109, "do_basic_setup"); + do_basic_setup(); + + /* Open the /dev/console on the rootfs, this should never fail */ ++ qkx_note_phase(-1, 110, "open_console"); + if (ksys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) + pr_err("Warning: unable to open an initial console.\n"); + +@@ -1186,6 +1492,7 @@ static noinline void __init kernel_init_freeable(void) + if (ksys_access((const char __user *) + ramdisk_execute_command, 0) != 0) { + ramdisk_execute_command = NULL; ++ qkx_note_phase(-1, 111, "prepare_namespace"); + prepare_namespace(); + } + +@@ -1198,6 +1505,12 @@ static noinline void __init kernel_init_freeable(void) + * and default modules + */ + ++ qkx_note_phase(-1, 112, "integrity_load_keys"); + integrity_load_keys(); ++ qkx_note_phase(-1, 113, "load_default_modules"); + load_default_modules(); ++ ++ if (!proc_create("qkx_bootdone", 0220, NULL, &qkx_bootdone_fops)) ++ pr_warn("QKX: failed to create /proc/qkx_bootdone\n"); ++ qkx_note_phase(99, 0, "userspace-init"); + } +diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c +index 23a44101cc..37f6731029 100644 +--- a/net/ipv4/ip_output.c ++++ b/net/ipv4/ip_output.c +@@ -1638,6 +1638,7 @@ void ip_send_unicast_reply(struct sock *sk, struct sk_buff *skb, + void __init ip_init(void) + { + ip_rt_init(); ++ + inet_initpeers(); + + #if defined(CONFIG_IP_MULTICAST) +diff --git a/techpack/camera/drivers/cam_cdm/cam_cdm_hw_core.c b/techpack/camera/drivers/cam_cdm/cam_cdm_hw_core.c +index dc493855a5..652c12c6a1 100644 +--- a/techpack/camera/drivers/cam_cdm/cam_cdm_hw_core.c ++++ b/techpack/camera/drivers/cam_cdm/cam_cdm_hw_core.c +@@ -28,6 +28,17 @@ + + #define CAM_CDM_BL_FIFO_WAIT_TIMEOUT 2000 + ++static int qkx_cpas_probe_stop = -1; ++core_param(qkx_cpas_probe_stop, qkx_cpas_probe_stop, int, 0444); ++ ++#define QKX_CPAS_STOP(_stage, _pdev) do { \ ++ if (qkx_cpas_probe_stop == (_stage)) { \ ++ pr_emerg("qkxcpas: %s diagnostic stop stage %d\n", \ ++ dev_name(&(_pdev)->dev), (_stage)); \ ++ return -ECANCELED; \ ++ } \ ++} while (0) ++ + static void cam_hw_cdm_work(struct work_struct *work); + + /* DT match table entry for all CDM variants*/ +@@ -913,7 +924,17 @@ int cam_hw_cdm_deinit(void *hw_priv, + soc_info = &cdm_hw->soc_info; + cdm_core = cdm_hw->core_info; + atomic_set(&cdm_core->work_record, 0); ++ if (qkx_cpas_probe_stop == 80) { ++ pr_emerg("qkxcpas: deinit diagnostic stop before disable_platform_resource\n"); ++ return -ECANCELED; ++ } ++ pr_emerg("qkxcpas: deinit before disable_platform_resource\n"); + rc = cam_soc_util_disable_platform_resource(soc_info, true, true); ++ pr_emerg("qkxcpas: deinit disable_platform_resource ret=%d\n", rc); ++ if (qkx_cpas_probe_stop == 81) { ++ pr_emerg("qkxcpas: deinit diagnostic stop after disable_platform_resource\n"); ++ return -ECANCELED; ++ } + if (rc) { + CAM_ERR(CAM_CDM, "disable platform failed"); + } else { +@@ -927,6 +948,8 @@ int cam_hw_cdm_deinit(void *hw_priv, + int cam_hw_cdm_probe(struct platform_device *pdev) + { + int rc; ++ ++ QKX_CPAS_STOP(0, pdev); + struct cam_hw_info *cdm_hw = NULL; + struct cam_hw_intf *cdm_hw_intf = NULL; + struct cam_cdm *cdm_core = NULL; +@@ -967,6 +990,7 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + CAM_ERR(CAM_CDM, "Failed to get dt properties"); + goto release_mem; + } ++ QKX_CPAS_STOP(1, pdev); + cdm_hw_intf->hw_idx = cdm_hw->soc_info.index; + cdm_core = (struct cam_cdm *)cdm_hw->core_info; + soc_private = (struct cam_cdm_private_dt_data *) +@@ -1003,11 +1027,15 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + + platform_set_drvdata(pdev, cdm_hw_intf); + ++ pr_emerg("qkxcpas: %s before cam_smmu_get_handle\n", dev_name(&pdev->dev)); + rc = cam_smmu_get_handle("cpas-cdm0", &cdm_core->iommu_hdl.non_secure); ++ pr_emerg("qkxcpas: %s cam_smmu_get_handle ret=%d handle=%d\n", ++ dev_name(&pdev->dev), rc, cdm_core->iommu_hdl.non_secure); + if (rc < 0) { + CAM_ERR(CAM_CDM, "cpas-cdm get iommu handle failed"); + goto unlock_release_mem; + } ++ QKX_CPAS_STOP(2, pdev); + cam_smmu_set_client_page_fault_handler(cdm_core->iommu_hdl.non_secure, + cam_hw_cdm_iommu_fault_handler, cdm_hw); + +@@ -1017,19 +1045,28 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + WQ_UNBOUND | WQ_MEM_RECLAIM | WQ_SYSFS, + CAM_CDM_INFLIGHT_WORKS); + ++ pr_emerg("qkxcpas: %s before request_platform_resource\n", ++ dev_name(&pdev->dev)); + rc = cam_soc_util_request_platform_resource(&cdm_hw->soc_info, + cam_hw_cdm_irq, cdm_hw); ++ pr_emerg("qkxcpas: %s request_platform_resource ret=%d\n", ++ dev_name(&pdev->dev), rc); + if (rc) { + CAM_ERR(CAM_CDM, "Failed to request platform resource"); + goto destroy_non_secure_hdl; + } ++ QKX_CPAS_STOP(3, pdev); + + cpas_parms.cam_cpas_client_cb = cam_cdm_cpas_cb; + cpas_parms.cell_index = cdm_hw->soc_info.index; + cpas_parms.dev = &pdev->dev; + cpas_parms.userdata = cdm_hw_intf; + strlcpy(cpas_parms.identifier, "cpas-cdm", CAM_HW_IDENTIFIER_LENGTH); ++ pr_emerg("qkxcpas: %s before cam_cpas_register_client\n", ++ dev_name(&pdev->dev)); + rc = cam_cpas_register_client(&cpas_parms); ++ pr_emerg("qkxcpas: %s cam_cpas_register_client ret=%d\n", ++ dev_name(&pdev->dev), rc); + if (rc) { + CAM_ERR(CAM_CDM, "Virtual CDM CPAS registration failed"); + goto release_platform_resource; +@@ -1037,6 +1074,7 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + CAM_DBG(CAM_CDM, "CPAS registration successful handle=%d", + cpas_parms.client_handle); + cdm_core->cpas_handle = cpas_parms.client_handle; ++ QKX_CPAS_STOP(4, pdev); + + axi_vote = kzalloc(sizeof(struct cam_axi_vote), GFP_KERNEL); + if (!axi_vote) { +@@ -1053,19 +1091,27 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + axi_vote->axi_path[0].mnoc_ab_bw = CAM_CPAS_DEFAULT_AXI_BW; + axi_vote->axi_path[0].mnoc_ib_bw = CAM_CPAS_DEFAULT_AXI_BW; + ++ pr_emerg("qkxcpas: %s before cam_cpas_start\n", dev_name(&pdev->dev)); + rc = cam_cpas_start(cdm_core->cpas_handle, &ahb_vote, axi_vote); ++ pr_emerg("qkxcpas: %s cam_cpas_start ret=%d\n", ++ dev_name(&pdev->dev), rc); + kfree(axi_vote); + if (rc) { + CAM_ERR(CAM_CDM, "CPAS start failed"); + goto cpas_unregister; + } ++ QKX_CPAS_STOP(5, pdev); + ++ pr_emerg("qkxcpas: %s before cam_hw_cdm_init\n", dev_name(&pdev->dev)); + rc = cam_hw_cdm_init(cdm_hw, NULL, 0); ++ pr_emerg("qkxcpas: %s cam_hw_cdm_init ret=%d\n", ++ dev_name(&pdev->dev), rc); + if (rc) { + CAM_ERR(CAM_CDM, "Failed to Init CDM HW"); + goto cpas_stop; + } + cdm_hw->open_count++; ++ QKX_CPAS_STOP(6, pdev); + + if (cam_cdm_read_hw_reg(cdm_hw, CDM_CFG_HW_VERSION, + &cdm_core->hw_version)) { +@@ -1079,6 +1125,7 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + goto deinit; + } + ++ QKX_CPAS_STOP(7, pdev); + CAM_DBG(CAM_CDM, "CDM Hw version read success family =%x hw =%x", + cdm_core->hw_family_version, cdm_core->hw_version); + cdm_core->ops = cam_cdm_get_ops(cdm_core->hw_version, NULL, +@@ -1100,6 +1147,7 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + cdm_hw->open_count--; + goto cpas_stop; + } ++ QKX_CPAS_STOP(8, pdev); + + rc = cam_cpas_stop(cdm_core->cpas_handle); + if (rc) { +@@ -1107,6 +1155,7 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + cdm_hw->open_count--; + goto cpas_unregister; + } ++ QKX_CPAS_STOP(9, pdev); + + rc = cam_cdm_intf_register_hw_cdm(cdm_hw_intf, + soc_private, CAM_HW_CDM, &cdm_core->index); +@@ -1115,6 +1164,7 @@ int cam_hw_cdm_probe(struct platform_device *pdev) + cdm_hw->open_count--; + goto cpas_unregister; + } ++ QKX_CPAS_STOP(10, pdev); + cdm_hw->open_count--; + mutex_unlock(&cdm_hw->hw_mutex); + +diff --git a/techpack/camera/drivers/cam_cpas/cam_cpas_soc.c b/techpack/camera/drivers/cam_cpas/cam_cpas_soc.c +index 3b4e4fb252..9d0a0df01a 100644 +--- a/techpack/camera/drivers/cam_cpas/cam_cpas_soc.c ++++ b/techpack/camera/drivers/cam_cpas/cam_cpas_soc.c +@@ -143,7 +143,10 @@ static int cam_cpas_update_camnoc_node(struct cam_cpas *cpas_core, + struct device_node *camnoc_node; + int rc; + +- camnoc_node = of_find_node_by_name(curr_node, ++ /* of_find_node_by_name() searches the rest of the whole tree, which ++ * picks up other nodes' ports when DT node order differs. ++ */ ++ camnoc_node = of_get_child_by_name(curr_node, + "qcom,axi-port-camnoc"); + if (camnoc_node) { + +@@ -192,7 +195,7 @@ static int cam_cpas_parse_node_tree(struct cam_cpas *cpas_core, + uint32_t client_idx = 0, cell_idx = 0, level_idx = 0; + int rc = 0, count = 0, i; + +- camera_bus_node = of_find_node_by_name(of_node, "camera-bus-nodes"); ++ camera_bus_node = of_get_child_by_name(of_node, "camera-bus-nodes"); + if (!camera_bus_node) { + CAM_ERR(CAM_CPAS, "Camera Bus node not found in cpas DT node"); + return -EINVAL; +@@ -262,7 +265,7 @@ static int cam_cpas_parse_node_tree(struct cam_cpas *cpas_core, + &curr_node_ptr->merge_type); + + curr_node_ptr->axi_port_idx = -1; +- mnoc_node = of_find_node_by_name(curr_node, ++ mnoc_node = of_get_child_by_name(curr_node, + "qcom,axi-port-mnoc"); + if (mnoc_node) { + if (mnoc_idx >= CAM_CPAS_MAX_AXI_PORTS) +diff --git a/techpack/camera/drivers/cam_sensor_module/cam_cci/cam_cci_soc.c b/techpack/camera/drivers/cam_sensor_module/cam_cci/cam_cci_soc.c +index 10f274c907..9d7c784d0b 100644 +--- a/techpack/camera/drivers/cam_sensor_module/cam_cci/cam_cci_soc.c ++++ b/techpack/camera/drivers/cam_sensor_module/cam_cci/cam_cci_soc.c +@@ -263,16 +263,16 @@ static void cam_cci_init_clk_params(struct cci_device *cci_dev) + for (count = 0; count < I2C_MAX_MODES; count++) { + + if (count == I2C_STANDARD_MODE) +- src_node = of_find_node_by_name(of_node, ++ src_node = of_get_child_by_name(of_node, + "qcom,i2c_standard_mode"); + else if (count == I2C_FAST_MODE) +- src_node = of_find_node_by_name(of_node, ++ src_node = of_get_child_by_name(of_node, + "qcom,i2c_fast_mode"); + else if (count == I2C_FAST_PLUS_MODE) +- src_node = of_find_node_by_name(of_node, ++ src_node = of_get_child_by_name(of_node, + "qcom,i2c_fast_plus_mode"); + else +- src_node = of_find_node_by_name(of_node, ++ src_node = of_get_child_by_name(of_node, + "qcom,i2c_custom_mode"); + + rc = of_property_read_u32(src_node, "hw-thigh", &val); +diff --git a/techpack/camera/drivers/cam_smmu/cam_smmu_api.c b/techpack/camera/drivers/cam_smmu/cam_smmu_api.c +index b249182af9..fc47ef53ba 100644 +--- a/techpack/camera/drivers/cam_smmu/cam_smmu_api.c ++++ b/techpack/camera/drivers/cam_smmu/cam_smmu_api.c +@@ -824,6 +824,9 @@ static int cam_smmu_create_add_handle_in_table(char *name, + + /* create handle and add in the iommu hardware table */ + for (i = 0; i < iommu_cb_set.cb_num; i++) { ++ /* Context-bank probes can be incomplete while dependencies defer. */ ++ if (!iommu_cb_set.cb_info[i].name) ++ continue; + if (!strcmp(iommu_cb_set.cb_info[i].name, name)) { + mutex_lock(&iommu_cb_set.cb_info[i].lock); + if (iommu_cb_set.cb_info[i].handle != HANDLE_INIT) { +diff --git a/techpack/display/msm/dsi/dsi_ctrl.c b/techpack/display/msm/dsi/dsi_ctrl.c +index acaa32e129..18177a81fe 100644 +--- a/techpack/display/msm/dsi/dsi_ctrl.c ++++ b/techpack/display/msm/dsi/dsi_ctrl.c +@@ -572,34 +572,34 @@ static int dsi_ctrl_clocks_deinit(struct dsi_ctrl *ctrl) + struct dsi_link_hs_clk_info *hs_link = &ctrl->clk_info.hs_link_clks; + struct dsi_clk_link_set *rcg = &ctrl->clk_info.rcg_clks; + +- if (core->mdp_core_clk) ++ if (!IS_ERR_OR_NULL(core->mdp_core_clk)) + devm_clk_put(&ctrl->pdev->dev, core->mdp_core_clk); +- if (core->iface_clk) ++ if (!IS_ERR_OR_NULL(core->iface_clk)) + devm_clk_put(&ctrl->pdev->dev, core->iface_clk); +- if (core->core_mmss_clk) ++ if (!IS_ERR_OR_NULL(core->core_mmss_clk)) + devm_clk_put(&ctrl->pdev->dev, core->core_mmss_clk); +- if (core->bus_clk) ++ if (!IS_ERR_OR_NULL(core->bus_clk)) + devm_clk_put(&ctrl->pdev->dev, core->bus_clk); +- if (core->mnoc_clk) ++ if (!IS_ERR_OR_NULL(core->mnoc_clk)) + devm_clk_put(&ctrl->pdev->dev, core->mnoc_clk); + + memset(core, 0x0, sizeof(*core)); + +- if (hs_link->byte_clk) ++ if (!IS_ERR_OR_NULL(hs_link->byte_clk)) + devm_clk_put(&ctrl->pdev->dev, hs_link->byte_clk); +- if (hs_link->pixel_clk) ++ if (!IS_ERR_OR_NULL(hs_link->pixel_clk)) + devm_clk_put(&ctrl->pdev->dev, hs_link->pixel_clk); +- if (lp_link->esc_clk) ++ if (!IS_ERR_OR_NULL(lp_link->esc_clk)) + devm_clk_put(&ctrl->pdev->dev, lp_link->esc_clk); +- if (hs_link->byte_intf_clk) ++ if (!IS_ERR_OR_NULL(hs_link->byte_intf_clk)) + devm_clk_put(&ctrl->pdev->dev, hs_link->byte_intf_clk); + + memset(hs_link, 0x0, sizeof(*hs_link)); + memset(lp_link, 0x0, sizeof(*lp_link)); + +- if (rcg->byte_clk) ++ if (!IS_ERR_OR_NULL(rcg->byte_clk)) + devm_clk_put(&ctrl->pdev->dev, rcg->byte_clk); +- if (rcg->pixel_clk) ++ if (!IS_ERR_OR_NULL(rcg->pixel_clk)) + devm_clk_put(&ctrl->pdev->dev, rcg->pixel_clk); + + memset(rcg, 0x0, sizeof(*rcg)); +@@ -1935,6 +1935,8 @@ static int dsi_ctrl_dev_probe(struct platform_device *pdev) + if (!dsi_ctrl) + return -ENOMEM; + ++ /* Cleanup paths use ctrl->pdev before successful probe completion. */ ++ dsi_ctrl->pdev = pdev; + dsi_ctrl->version = version; + dsi_ctrl->irq_info.irq_num = -1; + dsi_ctrl->irq_info.irq_stat_mask = 0x0; +diff --git a/techpack/display/msm/sde_power_handle.c b/techpack/display/msm/sde_power_handle.c +index 65d580765b..142be69859 100644 +--- a/techpack/display/msm/sde_power_handle.c ++++ b/techpack/display/msm/sde_power_handle.c +@@ -293,8 +293,10 @@ static int _sde_power_data_bus_set_quota( + int rc; + + if (pdbus->data_bus_hdl < 1) { +- pr_err("invalid bus handle %d\n", pdbus->data_bus_hdl); +- return -EINVAL; ++ /* qkx: data_bus_hdl==0 is now a normal, tolerated "no bus ++ * scaling available" state (see sde_power_data_bus_parse); ++ * skip the vote instead of failing the caller. */ ++ return 0; + } + + if (!in_ab_quota && !in_ib_quota) { +@@ -405,8 +407,10 @@ static int sde_power_data_bus_parse(struct platform_device *pdev, + pdbus->data_bus_hdl = msm_bus_scale_register_client( + pdbus->data_bus_scale_table); + if (!pdbus->data_bus_hdl) { +- pr_err("data_bus_client register failed\n"); +- rc = -EINVAL; ++ /* qkx: same msm_bus unavailability as sde_power_reg_bus_parse; ++ * continue without data-bus scaling rather than failing probe. */ ++ pr_warn("qkx: data_bus_client register failed; continuing without bus scaling\n"); ++ rc = 0; + goto end; + } + pr_debug("register %s data_bus_hdl=%x\n", name, pdbus->data_bus_hdl); +@@ -435,8 +439,17 @@ static int sde_power_reg_bus_parse(struct platform_device *pdev, + phandle->reg_bus_hdl = msm_bus_scale_register_client( + bus_scale_table); + if (!phandle->reg_bus_hdl) { +- pr_err("reg_bus_client register failed\n"); +- rc = -EINVAL; ++ /* ++ * qkx: after kexec msm_bus may never come up (deferred ++ * retries are disabled in this diagnostic kernel), which ++ * would leave the whole KMS/DRM device probe permanently ++ * dead. reg_bus_hdl==0 is already the "no bus scaling" ++ * state every other caller in this file guards against ++ * (sde_power_reg_bus_unregister/_update), so continue ++ * without it instead of failing the probe. ++ */ ++ pr_warn("qkx: reg_bus_client register failed; continuing without bus scaling\n"); ++ rc = 0; + goto end; + } + pr_debug("register reg_bus_hdl=%x\n", phandle->reg_bus_hdl); +@@ -528,6 +541,12 @@ int sde_power_resource_init(struct platform_device *pdev, + } + mp = &phandle->mp; + phandle->dev = &pdev->dev; ++ /* Deinit is called on deferred/failed probes, so synchronization and ++ * list state must be valid before the first operation that can fail. */ ++ INIT_LIST_HEAD(&phandle->event_list); ++ mutex_init(&phandle->phandle_lock); ++ phandle->rsc_client = NULL; ++ phandle->rsc_client_init = false; + + rc = sde_power_parse_dt_clock(pdev, mp); + if (rc) { +@@ -584,13 +603,6 @@ int sde_power_resource_init(struct platform_device *pdev, + else + pr_debug("cx ipeak client parse failed\n"); + +- INIT_LIST_HEAD(&phandle->event_list); +- +- phandle->rsc_client = NULL; +- phandle->rsc_client_init = false; +- +- mutex_init(&phandle->phandle_lock); +- + return rc; + + data_bus_err: +diff --git a/techpack/display/msm/sde_rsc.c b/techpack/display/msm/sde_rsc.c +index b6397a9650..8fc13816dc 100644 +--- a/techpack/display/msm/sde_rsc.c ++++ b/techpack/display/msm/sde_rsc.c +@@ -1551,8 +1551,9 @@ static int sde_rsc_probe(struct platform_device *pdev) + + rsc->fs = devm_regulator_get(&pdev->dev, "vdd"); + if (IS_ERR_OR_NULL(rsc->fs)) { ++ ret = !rsc->fs ? -ENODEV : PTR_ERR(rsc->fs); + rsc->fs = NULL; +- pr_err("unable to get regulator\n"); ++ pr_err("unable to get regulator, ret=%d\n", ret); + goto sde_rsc_fail; + } + +-- +2.55.0 + diff --git a/kernel/patches/0002-qkx-disable-stale-SPMI-IRQs-SMB5-skip-initial-status.patch b/kernel/patches/0002-qkx-disable-stale-SPMI-IRQs-SMB5-skip-initial-status.patch new file mode 100644 index 0000000..f0ef05b --- /dev/null +++ b/kernel/patches/0002-qkx-disable-stale-SPMI-IRQs-SMB5-skip-initial-status.patch @@ -0,0 +1,203 @@ +From 5d4273bfeb7ad4379dbbc93f4bd0fbbe495056d3 Mon Sep 17 00:00:00 2001 +From: yusuf +Date: Sat, 26 Sep 2026 21:06:25 +0300 +Subject: [PATCH 2/3] qkx: disable stale SPMI IRQs, SMB5 skip initial-status + handlers, probe_skip writable + +--- + drivers/base/dd.c | 3 ++- + drivers/power/supply/qcom/qpnp-smb5.c | 36 +++++++++++++++++++++++++++ + drivers/spmi/spmi-pmic-arb.c | 10 ++++++++ + 3 files changed, 48 insertions(+), 1 deletion(-) + +diff --git a/drivers/base/dd.c b/drivers/base/dd.c +index 2f822cdc3a..cf26ea61ff 100644 +--- a/drivers/base/dd.c ++++ b/drivers/base/dd.c +@@ -208,7 +208,8 @@ bool qkx_probe_trace; + static char *qkx_probe_skip = + "c440000.qcom,spmi:qcom,pm8150b@2:qcom,qpnp-smb5|" + "ac4d000.qcom,cpas-cdm0"; +-core_param(qkx_probe_skip, qkx_probe_skip, charp, 0444); ++/* Writable so a parked device can be released after USB logging is up. */ ++core_param(qkx_probe_skip, qkx_probe_skip, charp, 0644); + + static bool qkx_probe_is_skipped(const char *name) + { +diff --git a/drivers/power/supply/qcom/qpnp-smb5.c b/drivers/power/supply/qcom/qpnp-smb5.c +index f6e66a04d1..11df10f366 100644 +--- a/drivers/power/supply/qcom/qpnp-smb5.c ++++ b/drivers/power/supply/qcom/qpnp-smb5.c +@@ -3134,6 +3134,13 @@ static int smb5_post_init(struct smb5 *chip) + * DETERMINE INITIAL STATUS * + ****************************/ + ++/* qkx: kexec-handoff probe checkpoints; delay lets live log reach the host. */ ++#define QKX_SMB5_STEP(what) do { pr_emerg("qkxsmb5: before %s\n", what); msleep(100); } while (0) ++ ++/* qkx: skip hand-run IRQ handlers after kexec; real IRQs report the same state. */ ++static bool qkx_skip_initial; ++module_param(qkx_skip_initial, bool, 0444); ++ + static int smb5_determine_initial_status(struct smb5 *chip) + { + struct smb_irq_data irq_data = {chip, "determine-initial-status"}; +@@ -3151,16 +3158,32 @@ static int smb5_determine_initial_status(struct smb5 *chip) + if (chg->bms_psy) + smblib_suspend_on_debug_battery(chg); + ++ if (qkx_skip_initial) { ++ pr_emerg("qkxsmb5: skipping initial-status handlers\n"); ++ return 0; ++ } ++ ++ QKX_SMB5_STEP("usb_plugin_irq_handler"); + usb_plugin_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("dc_plugin_irq_handler"); + dc_plugin_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("typec_attach_detach_irq_handler"); + typec_attach_detach_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("typec_state_change_irq_handler"); + typec_state_change_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("usb_source_change_irq_handler"); + usb_source_change_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("chg_state_change_irq_handler"); + chg_state_change_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("icl_change_irq_handler"); + icl_change_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("batt_temp_changed_irq_handler"); + batt_temp_changed_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("wdog_bark_irq_handler"); + wdog_bark_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("typec_or_rid_detection_change_irq_handler"); + typec_or_rid_detection_change_irq_handler(0, &irq_data); ++ QKX_SMB5_STEP("wdog_snarl_irq_handler"); + wdog_snarl_irq_handler(0, &irq_data); + + return 0; +@@ -3717,6 +3740,7 @@ static int smb5_probe(struct platform_device *pdev) + return -EINVAL; + } + ++ QKX_SMB5_STEP("smb5_chg_config_init"); + rc = smb5_chg_config_init(chip); + if (rc < 0) { + if (rc != -EPROBE_DEFER) +@@ -3724,6 +3748,7 @@ static int smb5_probe(struct platform_device *pdev) + return rc; + } + ++ QKX_SMB5_STEP("smb5_parse_dt"); + rc = smb5_parse_dt(chip); + if (rc < 0) { + pr_err("Couldn't parse device tree rc=%d\n", rc); +@@ -3736,6 +3761,7 @@ static int smb5_probe(struct platform_device *pdev) + else + return -EPROBE_DEFER; + ++ QKX_SMB5_STEP("smblib_init"); + rc = smblib_init(chg); + if (rc < 0) { + pr_err("Smblib_init failed rc=%d\n", rc); +@@ -3776,6 +3802,7 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + ++ QKX_SMB5_STEP("smb5_init_hw"); + rc = smb5_init_hw(chip); + if (rc < 0) { + pr_err("Couldn't initialize hardware rc=%d\n", rc); +@@ -3791,6 +3818,7 @@ static int smb5_probe(struct platform_device *pdev) + * only. + */ + if (!chg->pd_not_supported) { ++ QKX_SMB5_STEP("smb5_init_vbus_regulator"); + rc = smb5_init_vbus_regulator(chip); + if (rc < 0) { + pr_err("Couldn't initialize vbus regulator rc=%d\n", +@@ -3798,6 +3826,7 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + ++ QKX_SMB5_STEP("smb5_init_vconn_regulator"); + rc = smb5_init_vconn_regulator(chip); + if (rc < 0) { + pr_err("Couldn't initialize vconn regulator rc=%d\n", +@@ -3805,6 +3834,7 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + ++ QKX_SMB5_STEP("smb5_init_otg_boost_regulator"); + rc = smb5_init_otg_boost_regulator(chip); + if (rc < 0) { + pr_err("Couldn't initialize otg boost regulator rc=%d\n", +@@ -3817,6 +3847,7 @@ static int smb5_probe(struct platform_device *pdev) + case PM8150B_SUBTYPE: + case PM6150_SUBTYPE: + case PM7250B_SUBTYPE: ++ QKX_SMB5_STEP("smb5_init_dc_psy"); + rc = smb5_init_dc_psy(chip); + if (rc < 0) { + pr_err("Couldn't initialize dc psy rc=%d\n", rc); +@@ -3827,6 +3858,7 @@ static int smb5_probe(struct platform_device *pdev) + break; + } + ++ QKX_SMB5_STEP("smb5_init_usb_psy"); + rc = smb5_init_usb_psy(chip); + if (rc < 0) { + pr_err("Couldn't initialize usb psy rc=%d\n", rc); +@@ -3845,18 +3877,21 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + ++ QKX_SMB5_STEP("smb5_init_batt_psy"); + rc = smb5_init_batt_psy(chip); + if (rc < 0) { + pr_err("Couldn't initialize batt psy rc=%d\n", rc); + goto cleanup; + } + ++ QKX_SMB5_STEP("smb5_init_typec_class"); + rc = smb5_init_typec_class(chip); + if (rc < 0) { + pr_err("Couldn't initialize typec class rc=%d\n", rc); + goto cleanup; + } + ++ QKX_SMB5_STEP("smb5_determine_initial_status"); + rc = smb5_determine_initial_status(chip); + if (rc < 0) { + pr_err("Couldn't determine initial status rc=%d\n", +@@ -3864,6 +3899,7 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + ++ QKX_SMB5_STEP("smb5_request_interrupts"); + rc = smb5_request_interrupts(chip); + if (rc < 0) { + pr_err("Couldn't request interrupts rc=%d\n", rc); +diff --git a/drivers/spmi/spmi-pmic-arb.c b/drivers/spmi/spmi-pmic-arb.c +index 14c1e38939..bfd53cee60 100644 +--- a/drivers/spmi/spmi-pmic-arb.c ++++ b/drivers/spmi/spmi-pmic-arb.c +@@ -491,6 +491,16 @@ static void cleanup_irq(struct spmi_pmic_arb *pmic_arb, u16 apid, int id) + dev_err_ratelimited(&pmic_arb->spmic->dev, "%s apid=%d sid=0x%x per=0x%x irq=%d\n", + __func__, apid, sid, per, id); + writel_relaxed(irq_mask, pmic_arb->ver_ops->irq_clear(pmic_arb, apid)); ++ /* qkx: Android leaves interrupts enabled in peripherals whose driver ++ * has not probed yet after kexec. Clearing alone lets a level source ++ * refire forever, so also disable it at the peripheral; the owning ++ * driver re-enables it through request_irq(). ++ */ ++ if (pmic_arb_write_cmd(pmic_arb->spmic, SPMI_CMD_EXT_WRITEL, sid, ++ (per << 8) + QPNPINT_REG_EN_CLR, &irq_mask, 1)) ++ dev_err_ratelimited(&pmic_arb->spmic->dev, ++ "failed to disable irq sid=0x%x per=0x%x irq=%d\n", ++ sid, per, id); + } + + static void periph_interrupt(struct spmi_pmic_arb *pmic_arb, u16 apid) +-- +2.55.0 + diff --git a/kernel/patches/0003-qkx-nothing-parked-by-default-SMB5-skips-initial-sta.patch b/kernel/patches/0003-qkx-nothing-parked-by-default-SMB5-skips-initial-sta.patch new file mode 100644 index 0000000..7d45a49 --- /dev/null +++ b/kernel/patches/0003-qkx-nothing-parked-by-default-SMB5-skips-initial-sta.patch @@ -0,0 +1,176 @@ +From 3f87f8ba54d8e48d4c0ba52a9c98e0e2063f2a07 Mon Sep 17 00:00:00 2001 +From: yusuf +Date: Sat, 26 Sep 2026 21:16:55 +0300 +Subject: [PATCH 3/3] qkx: nothing parked by default; SMB5 skips initial-status + handlers by default; drop SMB5 probe delays + +--- + drivers/base/dd.c | 4 +--- + drivers/power/supply/qcom/qpnp-smb5.c | 29 +-------------------------- + 2 files changed, 2 insertions(+), 31 deletions(-) + +diff --git a/drivers/base/dd.c b/drivers/base/dd.c +index cf26ea61ff..79b88557a7 100644 +--- a/drivers/base/dd.c ++++ b/drivers/base/dd.c +@@ -205,9 +205,7 @@ void device_unblock_probing(void) + /* qkx: explicit diagnostic kick enables slow per-probe tracing. */ + bool qkx_probe_trace; + /* Devices known to be unsafe or blocking when inherited across kexec. */ +-static char *qkx_probe_skip = +- "c440000.qcom,spmi:qcom,pm8150b@2:qcom,qpnp-smb5|" +- "ac4d000.qcom,cpas-cdm0"; ++static char *qkx_probe_skip = ""; + /* Writable so a parked device can be released after USB logging is up. */ + core_param(qkx_probe_skip, qkx_probe_skip, charp, 0644); + +diff --git a/drivers/power/supply/qcom/qpnp-smb5.c b/drivers/power/supply/qcom/qpnp-smb5.c +index 11df10f366..8deb7b4252 100644 +--- a/drivers/power/supply/qcom/qpnp-smb5.c ++++ b/drivers/power/supply/qcom/qpnp-smb5.c +@@ -3134,11 +3134,8 @@ static int smb5_post_init(struct smb5 *chip) + * DETERMINE INITIAL STATUS * + ****************************/ + +-/* qkx: kexec-handoff probe checkpoints; delay lets live log reach the host. */ +-#define QKX_SMB5_STEP(what) do { pr_emerg("qkxsmb5: before %s\n", what); msleep(100); } while (0) +- + /* qkx: skip hand-run IRQ handlers after kexec; real IRQs report the same state. */ +-static bool qkx_skip_initial; ++static bool qkx_skip_initial = true; + module_param(qkx_skip_initial, bool, 0444); + + static int smb5_determine_initial_status(struct smb5 *chip) +@@ -3163,27 +3160,16 @@ static int smb5_determine_initial_status(struct smb5 *chip) + return 0; + } + +- QKX_SMB5_STEP("usb_plugin_irq_handler"); + usb_plugin_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("dc_plugin_irq_handler"); + dc_plugin_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("typec_attach_detach_irq_handler"); + typec_attach_detach_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("typec_state_change_irq_handler"); + typec_state_change_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("usb_source_change_irq_handler"); + usb_source_change_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("chg_state_change_irq_handler"); + chg_state_change_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("icl_change_irq_handler"); + icl_change_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("batt_temp_changed_irq_handler"); + batt_temp_changed_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("wdog_bark_irq_handler"); + wdog_bark_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("typec_or_rid_detection_change_irq_handler"); + typec_or_rid_detection_change_irq_handler(0, &irq_data); +- QKX_SMB5_STEP("wdog_snarl_irq_handler"); + wdog_snarl_irq_handler(0, &irq_data); + + return 0; +@@ -3740,7 +3726,6 @@ static int smb5_probe(struct platform_device *pdev) + return -EINVAL; + } + +- QKX_SMB5_STEP("smb5_chg_config_init"); + rc = smb5_chg_config_init(chip); + if (rc < 0) { + if (rc != -EPROBE_DEFER) +@@ -3748,7 +3733,6 @@ static int smb5_probe(struct platform_device *pdev) + return rc; + } + +- QKX_SMB5_STEP("smb5_parse_dt"); + rc = smb5_parse_dt(chip); + if (rc < 0) { + pr_err("Couldn't parse device tree rc=%d\n", rc); +@@ -3761,7 +3745,6 @@ static int smb5_probe(struct platform_device *pdev) + else + return -EPROBE_DEFER; + +- QKX_SMB5_STEP("smblib_init"); + rc = smblib_init(chg); + if (rc < 0) { + pr_err("Smblib_init failed rc=%d\n", rc); +@@ -3802,7 +3785,6 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + +- QKX_SMB5_STEP("smb5_init_hw"); + rc = smb5_init_hw(chip); + if (rc < 0) { + pr_err("Couldn't initialize hardware rc=%d\n", rc); +@@ -3818,7 +3800,6 @@ static int smb5_probe(struct platform_device *pdev) + * only. + */ + if (!chg->pd_not_supported) { +- QKX_SMB5_STEP("smb5_init_vbus_regulator"); + rc = smb5_init_vbus_regulator(chip); + if (rc < 0) { + pr_err("Couldn't initialize vbus regulator rc=%d\n", +@@ -3826,7 +3807,6 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + +- QKX_SMB5_STEP("smb5_init_vconn_regulator"); + rc = smb5_init_vconn_regulator(chip); + if (rc < 0) { + pr_err("Couldn't initialize vconn regulator rc=%d\n", +@@ -3834,7 +3814,6 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + +- QKX_SMB5_STEP("smb5_init_otg_boost_regulator"); + rc = smb5_init_otg_boost_regulator(chip); + if (rc < 0) { + pr_err("Couldn't initialize otg boost regulator rc=%d\n", +@@ -3847,7 +3826,6 @@ static int smb5_probe(struct platform_device *pdev) + case PM8150B_SUBTYPE: + case PM6150_SUBTYPE: + case PM7250B_SUBTYPE: +- QKX_SMB5_STEP("smb5_init_dc_psy"); + rc = smb5_init_dc_psy(chip); + if (rc < 0) { + pr_err("Couldn't initialize dc psy rc=%d\n", rc); +@@ -3858,7 +3836,6 @@ static int smb5_probe(struct platform_device *pdev) + break; + } + +- QKX_SMB5_STEP("smb5_init_usb_psy"); + rc = smb5_init_usb_psy(chip); + if (rc < 0) { + pr_err("Couldn't initialize usb psy rc=%d\n", rc); +@@ -3877,21 +3854,18 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + +- QKX_SMB5_STEP("smb5_init_batt_psy"); + rc = smb5_init_batt_psy(chip); + if (rc < 0) { + pr_err("Couldn't initialize batt psy rc=%d\n", rc); + goto cleanup; + } + +- QKX_SMB5_STEP("smb5_init_typec_class"); + rc = smb5_init_typec_class(chip); + if (rc < 0) { + pr_err("Couldn't initialize typec class rc=%d\n", rc); + goto cleanup; + } + +- QKX_SMB5_STEP("smb5_determine_initial_status"); + rc = smb5_determine_initial_status(chip); + if (rc < 0) { + pr_err("Couldn't determine initial status rc=%d\n", +@@ -3899,7 +3873,6 @@ static int smb5_probe(struct platform_device *pdev) + goto cleanup; + } + +- QKX_SMB5_STEP("smb5_request_interrupts"); + rc = smb5_request_interrupts(chip); + if (rc < 0) { + pr_err("Couldn't request interrupts rc=%d\n", rc); +-- +2.55.0 + diff --git a/module/Makefile b/module/Makefile new file mode 100644 index 0000000..bfafd2a --- /dev/null +++ b/module/Makefile @@ -0,0 +1,23 @@ +# Out-of-tree build of the loader and the retained-log reader. +# +# make -C module KDIR= KSRC= +# +# KDIR must be a fully built (Image + modules) output directory of the kernel +# the headset is running, so Module.symvers carries the right CRCs. +ifneq ($(KERNELRELEASE),) +obj-m += quest_kexec.o marker_read.o +quest_kexec-y := loader.o transition.o +# Without this define the module can only stage (execute=1 is refused). +ccflags-y += -DQKX_ENABLE_EXPERIMENTAL_JUMP +else +KDIR ?= $(error set KDIR to the built Android kernel output directory) +LOCALVERSION ?= +KMAKE = $(MAKE) -C $(abspath $(KDIR)) M=$(CURDIR) ARCH=arm64 CC=clang LD=ld.lld \ + HOSTCC=clang CROSS_COMPILE=aarch64-linux-gnu- CLANG_TRIPLE=aarch64-linux-gnu- \ + LOCALVERSION=$(LOCALVERSION) +.PHONY: all clean +all: + $(KMAKE) modules +clean: + $(KMAKE) clean +endif diff --git a/module/loader.c b/module/loader.c new file mode 100644 index 0000000..bcd99a7 --- /dev/null +++ b/module/loader.c @@ -0,0 +1,1136 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Experimental Quest Pro ARM64 kexec module. Insertion stages by default. + * All inputs are regular files directly under /data/local/tmp. + * No partition/file writes and no sysfs command interface. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define QKX_LO 0x90000000ULL +#define QKX_HI 0x93400000ULL +#define QKX_ENTRY 0x90080000ULL +#define QKX_INITRD 0x93000000ULL +#define QKX_DTB 0x93200000ULL +/* Includes held allocations that land inside the destination window. */ +#define QKX_MAX_PAGES 32768 +#define QKX_DESC_COUNT ((PAGE_SIZE - 16) / 16) + +struct qkx_desc { + u64 next; + u64 count; + struct { u64 source, destination; } entry[QKX_DESC_COUNT]; +}; + +static char *image = "/data/local/tmp/qkx-Image"; +static char *initrd = "/data/local/tmp/qkx-initramfs"; +static char *dtb = "/data/local/tmp/qkx-boot.dtb"; +static bool execute; +static bool preserve_watchdog; +static bool watchdog_recovery; +static bool watchdog_control; +static bool test_transition; +static bool test_full_copy; +static unsigned int test_cpus; +static unsigned int phase_delay_ms; +static bool post_shutdown_probe; +static unsigned int preflight_only; +static unsigned int core_hang_control; +static unsigned int secure_watchdog_control; +static bool warm_reset_test; +static bool flush_rpmh; +static bool suspend_syncboss; +static bool disconnect_qmp; +module_param(image, charp, 0); +module_param(initrd, charp, 0); +module_param(dtb, charp, 0); +module_param(execute, bool, 0); +module_param(preserve_watchdog, bool, 0); +module_param(watchdog_recovery, bool, 0); +module_param(watchdog_control, bool, 0); +module_param(test_transition, bool, 0); +module_param(test_full_copy, bool, 0); +module_param(test_cpus, uint, 0); +module_param(phase_delay_ms, uint, 0); +module_param(post_shutdown_probe, bool, 0); +MODULE_PARM_DESC(post_shutdown_probe, "Run scratch physical roundtrip after shutdown before real jump"); +module_param(preflight_only, uint, 0); +module_param(core_hang_control, uint, 0); +module_param(secure_watchdog_control, uint, 0); +module_param(warm_reset_test, bool, 0); +MODULE_PARM_DESC(warm_reset_test, "Force the running Android kernel's Qualcomm warm-reboot path; no kexec staging"); +module_param(flush_rpmh, bool, 0); +MODULE_PARM_DESC(flush_rpmh, "Call the exported rpmh_flush() on the apps RSC before the final jump, mirroring the cluster-idle-enter path so the RSC hardware isn't left with stale active-set state across kexec"); +module_param(suspend_syncboss, bool, 0); +MODULE_PARM_DESC(suspend_syncboss, "Suspend spi0.0 syncboss through its PM callback before kexec, stopping active GPI DMA"); +module_param(disconnect_qmp, bool, 0); +MODULE_PARM_DESC(disconnect_qmp, "Cleanly close Android AOP QMP channels and publish mcore LINK_DOWN before kexec"); +MODULE_PARM_DESC(core_hang_control, "0: untouched; 1: report secure core-hang registers; 2: clear enable bit"); +MODULE_PARM_DESC(secure_watchdog_control, "0: untouched; 1: report SCM availability; 2: try supported secure-watchdog disable signatures"); +MODULE_PARM_DESC(phase_delay_ms, "Pause at pre-freeze checkpoints, maximum 5000 ms"); +MODULE_PARM_DESC(preflight_only, "1: preflight; 2: GPU rehearsal; 3: combined CPU/physical rehearsal"); +MODULE_PARM_DESC(preserve_watchdog, "Hand watchdog to target: bypass rejected secure disable"); +MODULE_PARM_DESC(watchdog_recovery, "Keep apps WDT armed for an automatic diagnostic reset instead of quiescing it"); +MODULE_PARM_DESC(watchdog_control, "After shutdown, latch a 10-second watchdog and spin for host timing calibration"); +MODULE_PARM_DESC(execute, "Experimental irreversible jump after staging (default false)"); + +extern void qkx_enter(u64 pgd, u64 code, u64 descriptors, u64 entry, u64 fdt) __noreturn; +extern const char qkx_reloc_start[], qkx_reloc_end[]; +extern const char qkx_probe_phys_start[], qkx_probe_phys_end[]; +extern const char qkx_reloc_return_start[], qkx_reloc_return_end[]; +extern unsigned long qkx_roundtrip(u64 pgd, u64 code, u64 descriptors, + u64 return_entry, u64 cookie); +static unsigned long (*lookup)(const char *); +static struct page **owned; +static unsigned int nr_owned, nr_copies; + +#define QKX_QMP_LINK_DOWN 0xFFFF0000 +#define QKX_QMP_CH_DISCONNECTED 0xFFFF0000 +#define QKX_QMP_LINK_CONNECTED 2 + +/* Private msm_qmp.c layouts from this exact Quest kernel build. They are used + * only to enumerate the live driver's mailboxes; state transitions themselves + * go through the driver's qmp_shutdown() and send_irq() implementations. */ +struct qkx_qmp_mbox { + struct list_head list; + struct mbox_controller ctrl; + int priority; + u32 num_assigned; + u32 num_shutdown; + void __iomem *desc; + bool rx_disabled; + bool tx_sent; + u32 idx_in_flight; + u32 mcore_mbox_offset; + u32 mcore_mbox_size; + struct qmp_pkt rx_pkt; + struct { u32 version, features; } version; + int local_state; + struct mutex state_lock; + spinlock_t tx_lock; + struct completion link_complete; + struct completion ch_complete; + struct delayed_work dwork; + void *mdev; +}; + +struct qkx_qmp_device { + struct device *dev; + const char *name; + struct list_head mboxes; +}; + +struct qkx_qmp_channel_desc { + u32 link_state; + u32 link_state_ack; + u32 ch_state; + u32 ch_state_ack; + u32 mailbox_size; + u32 mailbox_offset; +}; + +struct qkx_qmp_desc { + u32 magic; + u32 version; + u32 features; + struct qkx_qmp_channel_desc ucore; + struct qkx_qmp_channel_desc mcore; + u32 reserved; +}; + +static void qkx_copy_fromio32(void *dst, void __iomem *src, size_t len) +{ + u32 *d = dst; + + while (len >= sizeof(*d)) { + *d++ = ioread32(src); + src += sizeof(*d); + len -= sizeof(*d); + } +} + +static void qkx_copy_toio32(void __iomem *dst, const void *src, size_t len) +{ + const u32 *s = src; + + while (len >= sizeof(*s)) { + iowrite32(*s++, dst); + dst += sizeof(*s); + len -= sizeof(*s); + } +} +static struct qkx_desc *first_desc, *last_desc; +static void *code_page, *pgd_page; +static bool ram_found; +static atomic_t *qkx_pm_abort_suspend; +static atomic_t qkx_suppress_wakeup_abort = ATOMIC_INIT(0); + +static int qkx_wakeup_suppressor(void *unused) +{ + /* freeze_processes() clears pm_abort_suspend immediately before its + * loop. Keep it negative while that loop runs so USB/MCU wake IRQs cannot + * turn this irreversible reboot into a suspend-style -EBUSY abort. + */ + while (!kthread_should_stop() && atomic_read(&qkx_suppress_wakeup_abort)) { + atomic_set(qkx_pm_abort_suspend, -1000000); + cpu_relax(); + } + return 0; +} + +static int qkx_resolve(void) +{ + struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" }; + int ret = register_kprobe(&kp); + if (ret) + return ret; + lookup = (void *)kp.addr; + unregister_kprobe(&kp); + return 0; +} + +/* Hold rejected pages until unload: do not allocate/free the same page forever. */ +static void *qkx_page(void) +{ + while (nr_owned < QKX_MAX_PAGES) { + struct page *page = alloc_page(GFP_KERNEL | __GFP_ZERO | __GFP_RETRY_MAYFAIL); + phys_addr_t pa; + if (!page) + return NULL; + owned[nr_owned++] = page; + pa = page_to_phys(page); + if (pa < QKX_LO || pa >= QKX_HI) + return page_address(page); + } + return NULL; +} + +static int qkx_add(void *source, u64 dest) +{ + unsigned int n; + if (!last_desc || last_desc->count == QKX_DESC_COUNT) { + struct qkx_desc *d = qkx_page(); + if (!d) + return -ENOMEM; + if (last_desc) + last_desc->next = virt_to_phys(d); + else + first_desc = d; + last_desc = d; + } + n = last_desc->count++; + last_desc->entry[n].source = virt_to_phys(source); + last_desc->entry[n].destination = dest; + nr_copies++; + return 0; +} + +static struct file *qkx_open(const char *path) +{ + struct file *f; + const char *base; + if (strncmp(path, "/data/local/tmp/", 16)) + return ERR_PTR(-EPERM); + base = path + 16; + if (!*base || strchr(base, '/') || !strcmp(base, ".") || !strcmp(base, "..")) + return ERR_PTR(-EPERM); + f = filp_open(path, O_RDONLY | O_NOFOLLOW, 0); + if (!IS_ERR(f) && !S_ISREG(file_inode(f)->i_mode)) { + filp_close(f, NULL); + return ERR_PTR(-EINVAL); + } + return f; +} + +static int qkx_read(struct file *f, void *buf, size_t size, loff_t *pos) +{ + while (size) { + ssize_t n = kernel_read(f, buf, size, pos); + if (n <= 0) + return n < 0 ? n : -EIO; + buf += n; + size -= n; + } + return 0; +} + +static int qkx_stage(struct file *f, u64 dest, u64 file_size, u64 mem_size) +{ + loff_t pos = 0; + u64 off; + u32 crc = ~0U; + for (off = 0; off < mem_size; off += PAGE_SIZE) { + void *page = qkx_page(); + int ret; + if (!page) + return -ENOMEM; + if (off < file_size) { + ret = qkx_read(f, page, min_t(u64, PAGE_SIZE, file_size - off), &pos); + if (ret) + return ret; + crc = crc32_le(crc, page, min_t(u64, PAGE_SIZE, file_size - off)); + } + ret = qkx_add(page, dest + off); + if (ret) + return ret; + } + pr_info("quest_kexec: segment dest=%llx file=%llu memory=%llu crc32=%08x\n", + dest, file_size, mem_size, crc ^ ~0U); + return 0; +} + +static int qkx_ram_cb(struct resource *res, void *arg) +{ + /* walk_system_ram_res uses inclusive ends. */ + if (res->start <= QKX_LO && res->end >= QKX_HI - 1) + ram_found = true; + return 0; +} + +static int qkx_identity_map(void) +{ + u64 *pgd, *pud; + phys_addr_t pa; + size_t len = qkx_reloc_end - qkx_reloc_start; + BUILD_BUG_ON(PAGE_SIZE != 4096); + BUILD_BUG_ON(CONFIG_ARM64_VA_BITS != 39); + if (len > PAGE_SIZE) + return -E2BIG; + code_page = qkx_page(); + pgd_page = qkx_page(); + pud = qkx_page(); + if (!code_page || !pgd_page || !pud) + return -ENOMEM; + pgd = pgd_page; + pa = virt_to_phys(code_page); + /* 39-bit/4K: L1 table points to L2 table, which maps one 2M block. */ + pgd[(pa >> 30) & 511] = virt_to_phys(pud) | PUD_TYPE_TABLE; + pud[(pa >> 21) & 511] = (pa & PMD_MASK) | PMD_TYPE_SECT | + PMD_SECT_AF | PMD_SECT_S | PMD_ATTRINDX(MT_NORMAL); + memcpy(code_page, qkx_reloc_start, len); + return 0; +} + +static void qkx_release(void) +{ + while (nr_owned) + __free_page(owned[--nr_owned]); + vfree(owned); + owned = NULL; + first_desc = last_desc = NULL; +} + +static int qkx_test_transition(void); + +static int qkx_test_cpus(void) +{ + int (*freeze)(int) = (void *)lookup("freeze_secondary_cpus"); + void (*restore)(void) = (void *)lookup("enable_nonboot_cpus"); + int (*down)(unsigned int) = (void *)lookup("cpu_down"); + int (*up)(unsigned int) = (void *)lookup("cpu_up"); + int (*affinity)(struct task_struct *, const struct cpumask *) = + (void *)lookup("set_cpus_allowed_ptr"); + cpumask_t saved, online, removed; + int ret, affinity_ret, cpu; + if (!freeze || !restore || !affinity || !down || !up) + return -ENOENT; + cpumask_copy(&saved, ¤t->cpus_allowed); + cpumask_copy(&online, cpu_online_mask); + cpumask_clear(&removed); + if (!cpu_online(0)) + return -ENODEV; + ret = affinity(current, cpumask_of(0)); + if (ret) + return ret; + pr_info("quest_kexec: reversible CPU offline test begins; online=%u\n", + num_online_cpus()); + if (test_cpus == 1) { + ret = freeze(0); + } else { + for_each_cpu(cpu, &online) { + if (!cpu) + continue; + ret = down(cpu); + pr_info("quest_kexec: cpu_down(%d)=%d\n", cpu, ret); + if (ret) + break; + cpumask_set_cpu(cpu, &removed); + } + } + pr_info("quest_kexec: CPU freeze returned %d; online=%u\n", ret, + num_online_cpus()); + if (!ret && preflight_only == 3) { + if (num_online_cpus() != 1) + ret = -EBUSY; + else + ret = qkx_test_transition(); + } + /* Required even if freeze partially fails: balances hotplug disable. */ + if (test_cpus == 1) { + restore(); + } else { + for_each_cpu(cpu, &removed) { + int up_ret = up(cpu); + pr_info("quest_kexec: cpu_up(%d)=%d\n", cpu, up_ret); + if (up_ret) + ret = up_ret; + } + } + if (!cpumask_equal(&online, cpu_online_mask)) + ret = -EIO; + affinity_ret = affinity(current, &saved); + pr_info("quest_kexec: CPU restoration complete; online=%u affinity=%d result=%d\n", + num_online_cpus(), affinity_ret, ret); + return ret ? ret : affinity_ret; +} + +static int qkx_test_full_copy(void) +{ + struct qkx_desc *src, *head = NULL, *prev = NULL, *d; + unsigned int i, pages = 0, lists = 0; + unsigned long flags, result; + u64 start, elapsed; + size_t len = qkx_reloc_end - qkx_reloc_start; + size_t off = ALIGN(len, 16); + size_t end_len = qkx_reloc_return_end - qkx_reloc_return_start; + const unsigned long cookie = 0x514b5846554c4cUL; + if (off + end_len > PAGE_SIZE || read_sysreg(CurrentEL) != 4) + return -EINVAL; + /* Clone the production descriptor chain but replace EVERY destination + * with a separately owned page. Original source/target descriptors stay + * untouched; the real boot destination is never accessed here. + */ + for (src = first_desc; src; src = src->next ? phys_to_virt(src->next) : NULL) { + d = qkx_page(); + if (!d) + return -ENOMEM; + if (prev) + prev->next = virt_to_phys(d); + else + head = d; + prev = d; + lists++; + for (i = 0; i < src->count; i++) { + void *dst = qkx_page(); + const u8 *s = phys_to_virt(src->entry[i].source); + unsigned int j; + if (!dst) + return -ENOMEM; + /* Every byte initially differs, including zero-filled tails. */ + for (j = 0; j < PAGE_SIZE; j++) + ((u8 *)dst)[j] = s[j] ^ 0xff; + d->entry[i].source = src->entry[i].source; + d->entry[i].destination = virt_to_phys(dst); + d->count++; + pages++; + } + } + if (pages != nr_copies || !head) + return -EINVAL; + memset(code_page, 0, PAGE_SIZE); + memcpy(code_page, qkx_reloc_start, len); + memcpy(code_page + off, qkx_reloc_return_start, end_len); + for (i = 0; i < nr_owned; i++) + __flush_dcache_area(page_address(owned[i]), PAGE_SIZE); + __flush_icache_range((unsigned long)code_page, (unsigned long)code_page + PAGE_SIZE); + pr_emerg("quest_kexec: full scratch relocation starts: %u pages, %u descriptor pages\n", + pages, lists); + preempt_disable(); + local_irq_save(flags); + start = ktime_get_ns(); + result = qkx_roundtrip(virt_to_phys(pgd_page), virt_to_phys(code_page), + virt_to_phys(head), virt_to_phys(code_page) + off, cookie); + elapsed = ktime_get_ns() - start; + local_irq_restore(flags); + preempt_enable(); + if (result != cookie) + return -EIO; + for (d = head; d; d = d->next ? phys_to_virt(d->next) : NULL) + for (i = 0; i < d->count; i++) + if (memcmp(phys_to_virt(d->entry[i].source), + phys_to_virt(d->entry[i].destination), PAGE_SIZE)) { + pr_err("quest_kexec: full scratch copy mismatch\n"); + return -EIO; + } + pr_emerg("quest_kexec: full scratch relocation VERIFIED: %u pages, %u bytes, %llu us\n", + pages, pages * (unsigned int)PAGE_SIZE, elapsed / 1000); + return 0; +} + +static int qkx_test_transition(void) +{ + unsigned long irq_flags, result; + unsigned int i; + size_t probe_len = qkx_probe_phys_end - qkx_probe_phys_start; + size_t reloc_len = qkx_reloc_end - qkx_reloc_start; + size_t return_len = qkx_reloc_return_end - qkx_reloc_return_start; + size_t return_off = ALIGN(reloc_len, 16); + struct qkx_desc *desc; + void *source, *destination; + const unsigned long cookie = 0x514b5852454c4f43UL; /* QKXRELOC */ + if (probe_len > PAGE_SIZE || return_off + return_len > PAGE_SIZE || + read_sysreg(CurrentEL) != 4) + return -EINVAL; + memset(code_page, 0, PAGE_SIZE); + memcpy(code_page, qkx_probe_phys_start, probe_len); + for (i = 0; i < nr_owned; i++) + __flush_dcache_area(page_address(owned[i]), PAGE_SIZE); + __flush_icache_range((unsigned long)code_page, (unsigned long)code_page + PAGE_SIZE); + pr_info("quest_kexec: testing reversible MMU-off roundtrip\n"); + preempt_disable(); + local_irq_save(irq_flags); + result = qkx_roundtrip(virt_to_phys(pgd_page), virt_to_phys(code_page), 0, 0, 0); + local_irq_restore(irq_flags); + preempt_enable(); + pr_info("quest_kexec: physical roundtrip returned; disabled SCTLR flags=%lx\n", result); + if (result) + return -EIO; + + source = qkx_page(); + destination = qkx_page(); + desc = qkx_page(); + if (!source || !destination || !desc) + return -ENOMEM; + for (i = 0; i < PAGE_SIZE; i++) + ((u8 *)source)[i] = (u8)(i * 131 + 17); + memset(destination, 0xa5, PAGE_SIZE); + desc->count = 1; + desc->entry[0].source = virt_to_phys(source); + desc->entry[0].destination = virt_to_phys(destination); + + memset(code_page, 0, PAGE_SIZE); + memcpy(code_page, qkx_reloc_start, reloc_len); + memcpy(code_page + return_off, qkx_reloc_return_start, return_len); + for (i = 0; i < nr_owned; i++) + __flush_dcache_area(page_address(owned[i]), PAGE_SIZE); + __flush_icache_range((unsigned long)code_page, (unsigned long)code_page + PAGE_SIZE); + pr_info("quest_kexec: testing physical relocation loop on one scratch page\n"); + preempt_disable(); + local_irq_save(irq_flags); + result = qkx_roundtrip(virt_to_phys(pgd_page), virt_to_phys(code_page), + virt_to_phys(desc), virt_to_phys(code_page) + return_off, cookie); + local_irq_restore(irq_flags); + preempt_enable(); + if (result != cookie) { + pr_err("quest_kexec: relocation returned wrong cookie: %lx\n", result); + return -EIO; + } + if (memcmp(source, destination, PAGE_SIZE)) { + pr_err("quest_kexec: scratch relocation data mismatch\n"); + return -EIO; + } + pr_info("quest_kexec: physical relocation loop returned and copied scratch page correctly\n"); + return 0; +} + +/* This path is deliberately separate from staging. It has not been validated + * on Quest firmware. In particular Qualcomm DMA and watchdog shutdown need a + * device-specific review before using execute=1 on hardware. + */ +static void qkx_phase(unsigned int phase, const char *message, bool may_pause) +{ + pr_emerg("quest_kexec: PHASE %02u: %s\n", phase, message); + if (may_pause && phase_delay_ms) + msleep(phase_delay_ms); +} + +static int qkx_disconnect_aop_qmp(struct device *dev) +{ + struct qkx_qmp_device *mdev = dev_get_drvdata(dev); + void (*shutdown)(struct mbox_chan *) = (void *)lookup("qmp_shutdown"); + void (*send_irq_fn)(void *) = (void *)lookup("send_irq"); + struct qkx_qmp_mbox *mbox; + unsigned int i, wait; + + if (!mdev || !shutdown || !send_irq_fn || !mdev->name) + return -ENOENT; + if (list_empty(&mdev->mboxes)) + return -ENODEV; + + list_for_each_entry(mbox, &mdev->mboxes, list) { + u32 assigned = READ_ONCE(mbox->num_assigned); + struct qkx_qmp_desc desc; + + if (!mbox->desc || assigned > mbox->ctrl.num_chans) + return -EINVAL; + pr_emerg("quest_kexec: QMP %s closing %u assigned channels state=%d\n", + mdev->name, assigned, READ_ONCE(mbox->local_state)); + for (i = 0; i < assigned; i++) + shutdown(&mbox->ctrl.chans[i]); + + /* qmp_shutdown() completes asynchronously in the driver's RX worker. */ + for (wait = 0; wait < 100; wait++) { + if (READ_ONCE(mbox->local_state) == QKX_QMP_LINK_CONNECTED) + break; + msleep(10); + } + if (READ_ONCE(mbox->local_state) != QKX_QMP_LINK_CONNECTED) + return -ETIMEDOUT; + + qkx_copy_fromio32(&desc, mbox->desc, sizeof(desc)); + desc.mcore.link_state = QKX_QMP_LINK_DOWN; + desc.mcore.ch_state = QKX_QMP_CH_DISCONNECTED; + desc.mcore.ch_state_ack = QKX_QMP_CH_DISCONNECTED; + qkx_copy_toio32(mbox->desc + offsetof(struct qkx_qmp_desc, mcore), + &desc.mcore, sizeof(desc.mcore)); + send_irq_fn(mdev); + + for (wait = 0; wait < 100; wait++) { + if (ioread32(mbox->desc + + offsetof(struct qkx_qmp_desc, mcore.link_state_ack)) == + QKX_QMP_LINK_DOWN) + break; + msleep(10); + } + qkx_copy_fromio32(&desc, mbox->desc, sizeof(desc)); + pr_emerg("quest_kexec: QMP %s down result mlink=%08x mack=%08x mch=%08x mcack=%08x\n", + mdev->name, desc.mcore.link_state, + desc.mcore.link_state_ack, desc.mcore.ch_state, + desc.mcore.ch_state_ack); + if (desc.mcore.link_state_ack != QKX_QMP_LINK_DOWN) + return -ETIMEDOUT; + } + return 0; +} + +static int __maybe_unused qkx_execute(void) +{ + void (*prepare)(char *) = (void *)lookup("kernel_restart_prepare"); + void (*migrate)(void) = (void *)lookup("migrate_to_reboot_cpu"); + void (*hotplug_enable)(void) = (void *)lookup("cpu_hotplug_enable"); + void (*shutdown_cpus)(void) = (void *)lookup("machine_shutdown"); + bool *kexec_progress = (void *)lookup("kexec_in_progress"); + bool (*stuck)(void) = (void *)lookup("cpus_are_stuck_in_kernel"); + const u32 *mode = (void *)lookup("__boot_cpu_mode"); + int (*freeze_users)(void) = (void *)lookup("freeze_processes"); + void (*thaw_users)(void) = (void *)lookup("thaw_processes"); + void (*sleep_lock)(void) = (void *)lookup("lock_system_sleep"); + void (*sleep_unlock)(void) = (void *)lookup("unlock_system_sleep"); + bool *events_check_enabled = (void *)lookup("events_check_enabled"); + int (*gpu_suspend)(struct device *) = (void *)lookup("adreno_pm_suspend"); + int (*gpu_resume)(struct device *) = (void *)lookup("adreno_pm_resume"); + ssize_t (*watchdog_disable)(struct device *, struct device_attribute *, + const char *, size_t) = (void *)lookup("wdog_disable_set"); + struct bus_type *platform_bus = (void *)lookup("platform_bus_type"); + struct bus_type *spi_bus = (void *)lookup("spi_bus_type"); + struct device *(*find_dev)(struct bus_type *, struct device *, const char *) = + (void *)lookup("bus_find_device_by_name"); + int (*suspend_dwc3_gadget)(void *) = + (void *)lookup("dwc3_gadget_suspend"); + int (*rpmh_flush_fn)(const struct device *) = + (void *)lookup("rpmh_flush"); + struct device *gpu, *watchdog, *usb_core = NULL, *rpmh_dev = NULL; + struct device *syncboss = NULL, *qmp_dev = NULL; + struct task_struct *wakeup_suppressor = NULL; + bool syncboss_suspended = false; + void __iomem *wdt_regs = NULL; + bool sleep_locked = false; + unsigned int i; + int ret; + qkx_phase(1, "symbol lookups returned", true); + if (!prepare || !migrate || !hotplug_enable || !shutdown_cpus || + !stuck || !mode) + return -ENOENT; + pr_emerg("quest_kexec: native shutdown symbols ready; kexec_in_progress=%s\n", + kexec_progress ? "available" : "compiled out"); + qkx_pm_abort_suspend = (void *)lookup("pm_abort_suspend"); + if (!freeze_users || !thaw_users || !sleep_lock || !sleep_unlock || + !events_check_enabled || !qkx_pm_abort_suspend || !gpu_suspend || !gpu_resume || + !watchdog_disable || !platform_bus || !find_dev || + !suspend_dwc3_gadget || (suspend_syncboss && !spi_bus)) + return -ENOENT; + /* No EL2 recovery/HVC assumptions: this implementation supports EL1 boot. */ + if (read_sysreg(CurrentEL) != 4 || mode[0] != 0xe11 || mode[1] != 0xe11) + return -EOPNOTSUPP; + if (stuck()) + return -EBUSY; + qkx_phase(2, "EL1 and CPU checks passed; looking up GPU", true); + gpu = find_dev(platform_bus, NULL, "3d00000.qcom,kgsl-3d0"); + qkx_phase(3, "GPU lookup returned; looking up watchdog", true); + watchdog = find_dev(platform_bus, NULL, "17c10000.qcom,wdt"); + usb_core = find_dev(platform_bus, NULL, "a600000.dwc3"); + if (suspend_syncboss) + syncboss = find_dev(spi_bus, NULL, "spi0.0"); + if (disconnect_qmp) + qmp_dev = find_dev(platform_bus, NULL, "c300000.qcom,qmp-aop"); + if (flush_rpmh) + rpmh_dev = find_dev(platform_bus, NULL, + "18200000.rsc:rpmh-regulator-mmcxlvl"); + if (flush_rpmh && (!rpmh_flush_fn || !rpmh_dev)) { + pr_emerg("quest_kexec: flush_rpmh requested but symbol=%d dev=%d; disabling\n", + !!rpmh_flush_fn, !!rpmh_dev); + put_device(rpmh_dev); + rpmh_dev = NULL; + } + if (!gpu || !watchdog || !usb_core || !dev_get_drvdata(usb_core) || + (disconnect_qmp && (!qmp_dev || !dev_get_drvdata(qmp_dev))) || + (suspend_syncboss && (!syncboss || !syncboss->driver || + !syncboss->driver->pm || !syncboss->driver->pm->suspend))) { + put_device(gpu); + put_device(watchdog); + put_device(usb_core); + put_device(rpmh_dev); + put_device(syncboss); + put_device(qmp_dev); + return -ENODEV; + } + qkx_phase(4, "devices found; mapping watchdog if requested", true); + if (preserve_watchdog) { + /* The exact seacliff device name above and captured DT identify this + * block. Only WDT0_RST is written; timeouts and enable are preserved. + */ + wdt_regs = ioremap(0x17c10000, SZ_4K); + if (!wdt_regs) { + ret = -ENOMEM; + goto release_devices; + } + qkx_phase(5, "watchdog mapped; reading registers", true); + pr_emerg("quest_kexec: preserving watchdog en=%x bark=%u bite=%u\n", + readl_relaxed(wdt_regs + 0x08), readl_relaxed(wdt_regs + 0x10), + readl_relaxed(wdt_regs + 0x14)); + } + qkx_phase(6, "preflight complete; next operation freezes userspace", true); + if (preflight_only == 1) { + pr_emerg("quest_kexec: preflight-only test complete; returning without shutdown\n"); + ret = 0; + goto release_devices; + } + qkx_phase(7, "acquiring system-sleep lock before freezing userspace", false); + sleep_lock(); + sleep_locked = true; + /* This is an irreversible reboot, not suspend. Android autosleep can arm + * wakeup checking before blocking on system_transition_mutex, leaving a + * stale saved_count that makes the direct freezer abort. Suppress that + * suspend-only abort check while holding the system-sleep lock; + * freeze_processes still clears pm_abort_suspend itself. + */ + WRITE_ONCE(*events_check_enabled, false); + atomic_set(&qkx_suppress_wakeup_abort, 1); + wakeup_suppressor = kthread_run(qkx_wakeup_suppressor, NULL, + "qkx_wakeup_suppress"); + if (IS_ERR(wakeup_suppressor)) { + ret = PTR_ERR(wakeup_suppressor); + wakeup_suppressor = NULL; + atomic_set(&qkx_suppress_wakeup_abort, 0); + goto release_devices; + } + /* Ensure the suppressor has run before freeze_processes clears the flag. */ + while (atomic_read(qkx_pm_abort_suspend) >= 0) + cpu_relax(); + pr_emerg("quest_kexec: system-sleep lock acquired; wakeup abort check suppressed; freezing userspace\n"); + ret = freeze_users(); + atomic_set(&qkx_suppress_wakeup_abort, 0); + kthread_stop(wakeup_suppressor); + wakeup_suppressor = NULL; + atomic_set(qkx_pm_abort_suspend, 0); + pr_emerg("quest_kexec: PHASE 08: freeze_processes returned %d\n", ret); + /* freeze_processes already thaws everything on failure. */ + if (ret) { + goto release_devices; + } + qkx_phase(9, "calling GPU suspend", false); + ret = gpu_suspend(gpu); + pr_emerg("quest_kexec: PHASE 10: GPU suspend returned %d\n", ret); + if (ret) { + gpu_resume(gpu); + thaw_users(); + goto release_devices; + } + if (suspend_syncboss) { + qkx_phase(10, "suspending Android syncboss SPI streaming", false); + ret = syncboss->driver->pm->suspend(syncboss); + pr_emerg("quest_kexec: syncboss suspend returned %d\n", ret); + if (ret) { + gpu_resume(gpu); + thaw_users(); + goto release_devices; + } + syncboss_suspended = true; + } + if (preflight_only >= 2) { + int resume_ret; + if (preflight_only == 3) + ret = qkx_test_cpus(); + resume_ret = gpu_resume(gpu); + thaw_users(); + pr_emerg("quest_kexec: rehearsal restored userspace; GPU resume=%d test=%d\n", + resume_ret, ret); + if (!ret) + ret = resume_ret; + goto release_devices; + } + /* Stop gadget traffic and controller Run/Stop without tearing down the + * Qualcomm glue power domain. Full driver unbind hard-resets the board. */ + qkx_phase(10, "suspending Android DWC3 gadget", false); + ret = suspend_dwc3_gadget(dev_get_drvdata(usb_core)); + pr_emerg("quest_kexec: DWC3 gadget suspend returned %d\n", ret); + if (ret) + panic("quest_kexec: DWC3 gadget suspend failed (%d)", ret); + put_device(usb_core); + usb_core = NULL; + + /* Invoke the driver's complete operation: secure SCM call, disable mutex, + * application watchdog, IRQ and timer teardown. No sysfs write is used. + */ + if (preserve_watchdog) { + pr_emerg("quest_kexec: deferring apps-watchdog quiesce until final jump\n"); + ret = 1; + } else { + pr_emerg("quest_kexec: disabling secure and application watchdogs\n"); + ret = watchdog_disable(watchdog, NULL, "1", 1); + } + if (ret != 1) { + if (ret >= 0) + ret = -EIO; + gpu_resume(gpu); + thaw_users(); + goto release_devices; + } + if (disconnect_qmp) { + qkx_phase(109, "cleanly disconnecting Android AOP QMP", false); + ret = qkx_disconnect_aop_qmp(qmp_dev); + pr_emerg("quest_kexec: AOP QMP disconnect returned %d\n", ret); + if (ret) + panic("quest_kexec: AOP QMP disconnect failed (%d)", ret); + } + put_device(qmp_dev); + qmp_dev = NULL; + put_device(gpu); + put_device(watchdog); + /* Match kernel_kexec(): advertise kexec before device shutdown so PCI + * and any other kexec-aware shutdown callbacks choose the right path. */ + if (kexec_progress) + WRITE_ONCE(*kexec_progress, true); + qkx_phase(11, "calling kernel_restart_prepare; irreversible shutdown", false); + prepare(NULL); + qkx_phase(12, "device shutdown returned; migrating to CPU0", false); + migrate(); + /* migrate_to_reboot_cpu() disables hotplug; native kernel_kexec() + * reenables it immediately before machine_shutdown(). */ + hotplug_enable(); + /* freeze_secondary_cpus() rechecks pm_wakeup_pending() per CPU; a late + * wake IRQ during device shutdown would abort it and force a panic. */ + WRITE_ONCE(*events_check_enabled, false); + atomic_set(qkx_pm_abort_suspend, 0); + qkx_phase(13, "calling native machine_shutdown", false); + shutdown_cpus(); + qkx_phase(14, "native machine_shutdown returned", false); + if (num_online_cpus() != 1 || stuck()) + panic("quest_kexec: native CPU shutdown failed"); + if (rpmh_dev) { + int rpmh_ret; + + /* qkx: mirror system_pm.c's system_sleep_enter(), which flushes + * every cached RPMh vote to the RSC's sleep/wake TCS banks before + * the last CPU stops running. A normal Android reboot never needs + * this (a real SoC reset re-inits the RSC hardware via PSHOLD/PBL + * before the new kernel boots); our kexec never resets the RSC at + * all, so without this the hardware is left holding whatever + * active-set state the dying kernel had, which is suspected to + * cause the intermittent post-kexec RPMh/SPMI stalls seen across + * runs. rpmh_flush() is a no-op if nothing is dirty. + */ + qkx_phase(140, "flushing RPMh votes to RSC before final jump", false); + rpmh_ret = rpmh_flush_fn(rpmh_dev); + pr_emerg("quest_kexec: rpmh_flush returned %d\n", rpmh_ret); + qkx_phase(141, "rpmh_flush returned", false); + put_device(rpmh_dev); + rpmh_dev = NULL; + } + if (post_shutdown_probe) { + qkx_phase(16, "post-shutdown physical scratch probe begins", false); + msleep(50); + ret = qkx_test_transition(); + pr_emerg("quest_kexec: PHASE 17: post-shutdown physical scratch result=%d\n", ret); + msleep(50); + if (ret) + panic("quest_kexec: post-shutdown scratch probe failed (%d)", ret); + /* Probe appended a return stub; restore the production code image. */ + memset(code_page, 0, PAGE_SIZE); + memcpy(code_page, qkx_reloc_start, qkx_reloc_end - qkx_reloc_start); + } + if (wdt_regs) { + if (watchdog_recovery) { + /* Retain the normal 12/15-second watchdog for this diagnostic + * attempt so a warm reset preserves the early-entry RAM marker. + */ + writel_relaxed(1, wdt_regs + 0x04); + mb(); + pr_emerg("quest_kexec: apps watchdog armed for diagnostic recovery; EN=%x\n", + readl_relaxed(wdt_regs + 0x08)); + } else { + /* Stop the apps watchdog while normal mappings and USB diagnostics + * are still available. The target watchdog probe re-enables it. + */ + writel_relaxed(0, wdt_regs + 0x08); + mb(); + pr_emerg("quest_kexec: apps watchdog quiesced; EN readback=%x\n", + readl_relaxed(wdt_regs + 0x08)); + } + } + qkx_phase(15, "single CPU; flushing payload and entering physical trampoline", false); + if (watchdog_control) { + /* Second calibration point: exactly twice the first control's ticks. */ + writel_relaxed(0, wdt_regs + 0x08); + mb(); + writel_relaxed(622535, wdt_regs + 0x10); + writel_relaxed(655300, wdt_regs + 0x14); + writel_relaxed(3, wdt_regs + 0x08); + writel_relaxed(1, wdt_regs + 0x04); + mb(); + pr_emerg("quest_kexec: watchdog timing control armed; spinning\n"); + local_irq_disable(); + for (;;) + cpu_relax(); + } + /* Let the direct USB reader observe CPU7 completion and this checkpoint + * before interrupts are masked. This is not an acknowledgment protocol. + */ + msleep(50); + local_irq_disable(); + preempt_disable(); + /* Everything the physical code reads must be clean to PoC. */ + for (i = 0; i < nr_owned; i++) + __flush_dcache_area(page_address(owned[i]), PAGE_SIZE); + __flush_icache_range((unsigned long)code_page, (unsigned long)code_page + PAGE_SIZE); + qkx_enter(virt_to_phys(pgd_page), virt_to_phys(code_page), + virt_to_phys(first_desc), QKX_ENTRY, QKX_DTB); +release_devices: + if (syncboss_suspended && syncboss && syncboss->driver && + syncboss->driver->pm && syncboss->driver->pm->resume) + syncboss->driver->pm->resume(syncboss); + if (wakeup_suppressor) { + atomic_set(&qkx_suppress_wakeup_abort, 0); + kthread_stop(wakeup_suppressor); + atomic_set(qkx_pm_abort_suspend, 0); + } + if (sleep_locked) + sleep_unlock(); + if (wdt_regs) + iounmap(wdt_regs); + put_device(gpu); + put_device(watchdog); + put_device(usb_core); + put_device(rpmh_dev); + put_device(syncboss); + put_device(qmp_dev); + return ret; +} + +static int qkx_core_hang_configure(void) +{ + static const phys_addr_t config[] = { + 0x18000060, 0x18010060, 0x18020060, 0x18030060, + 0x18040060, 0x18050060, 0x18060060, 0x18070060, + }; + u32 (*secure_read)(phys_addr_t) = (void *)lookup("scm_io_read"); + int (*secure_write)(phys_addr_t, u32) = (void *)lookup("scm_io_write"); + unsigned int i; + + if (!secure_read || (core_hang_control == 2 && !secure_write)) + return -ENOENT; + for (i = 0; i < ARRAY_SIZE(config); i++) { + u32 before = secure_read(config[i]); + int ret = 0; + u32 after = before; + + if (core_hang_control == 2 && (before & BIT(1))) { + ret = secure_write(config[i], before & ~BIT(1)); + after = secure_read(config[i]); + } + pr_emerg("quest_kexec: core-hang cpu%u config=%pa before=%08x after=%08x write=%d\n", + i, &config[i], before, after, ret); + if (ret || (core_hang_control == 2 && (after & BIT(1)))) + return ret ? ret : -EIO; + } + return 0; +} + +static int qkx_secure_watchdog_configure(void) +{ + int (*available)(u32, u32) = (void *)lookup("scm_is_call_available"); + int (*call)(u32, struct scm_desc *) = (void *)lookup("scm_call2"); + struct scm_desc desc = { }; + int avail, ret; + + if (!available || !call) + return -ENOENT; + avail = available(SCM_SVC_BOOT, 0x7); + pr_emerg("quest_kexec: secure-watchdog disable SCM availability=%d\n", avail); + if (secure_watchdog_control != 2) + return avail < 0 ? avail : 0; + + /* This firmware rejects the downstream driver's one-argument value 1. + * Probe the older no-argument and one-argument-zero ABIs in that order. + */ + desc.arginfo = SCM_ARGS(0); + ret = call(SCM_SIP_FNID(SCM_SVC_BOOT, 0x7), &desc); + pr_emerg("quest_kexec: secure-watchdog disable noarg ret=%d secure=%llx/%llx/%llx\n", + ret, desc.ret[0], desc.ret[1], desc.ret[2]); + if (!ret) + return 0; + memset(&desc, 0, sizeof(desc)); + desc.arginfo = SCM_ARGS(1); + desc.args[0] = 0; + ret = call(SCM_SIP_FNID(SCM_SVC_BOOT, 0x7), &desc); + pr_emerg("quest_kexec: secure-watchdog disable arg0 ret=%d secure=%llx/%llx/%llx\n", + ret, desc.ret[0], desc.ret[1], desc.ret[2]); + return ret; +} + +static int __init qkx_init(void) +{ + struct file *kf = NULL, *rf = NULL, *df = NULL; + u8 kh[64], dh[40]; + u64 ksize, rsize, dsize, image_size, flags; + loff_t pos = 0; + int ret; + int (*walk_ram)(u64, u64, void *, int (*)(struct resource *, void *)); + if (test_cpus > 2 || preflight_only > 3 || core_hang_control > 2 || + secure_watchdog_control > 2 || phase_delay_ms > 5000 || + (preflight_only && !execute) || (post_shutdown_probe && !execute) || + (watchdog_recovery && (!execute || !preserve_watchdog)) || + (watchdog_control && !watchdog_recovery)) + return -EINVAL; + if ((execute && (test_transition || test_cpus)) || (test_transition && test_cpus)) + return -EINVAL; + if (test_full_copy && (execute || test_transition || test_cpus)) + return -EINVAL; +#ifndef QKX_ENABLE_EXPERIMENTAL_JUMP + if (execute) { + pr_err("quest_kexec: jump disabled in this staging build; board shutdown is unvalidated\n"); + return -EOPNOTSUPP; + } +#endif + if (!of_machine_is_compatible("qcom,kona")) + return -ENODEV; + ret = qkx_resolve(); + if (ret) + return ret; + if (warm_reset_test) { + bool *force_warm = (void *)lookup("force_warm_reboot"); + void (*restart)(char *) = (void *)lookup("machine_restart"); + + if (!force_warm || !restart) + return -ENOENT; + WRITE_ONCE(*force_warm, true); + pr_emerg("quest_kexec: forcing Qualcomm warm-reset calibration now\n"); + msleep(100); + restart(NULL); + return -EIO; + } + if (core_hang_control) { + ret = qkx_core_hang_configure(); + if (ret) + return ret; + } + if (secure_watchdog_control) { + ret = qkx_secure_watchdog_configure(); + if (ret) + return ret; + } + walk_ram = (void *)lookup("walk_system_ram_res"); + if (!walk_ram) + return -ENOENT; + ret = walk_ram(QKX_LO, QKX_HI - 1, NULL, qkx_ram_cb); + if (ret || !ram_found) + return -ERANGE; + /* Host prepare_boot.py also rejects nested reserved iomem regions. */ + owned = vzalloc(QKX_MAX_PAGES * sizeof(*owned)); + if (!owned) + return -ENOMEM; + kf = qkx_open(image); + if (IS_ERR(kf)) { ret = PTR_ERR(kf); kf = NULL; goto out; } + rf = qkx_open(initrd); + if (IS_ERR(rf)) { ret = PTR_ERR(rf); rf = NULL; goto out; } + df = qkx_open(dtb); + if (IS_ERR(df)) { ret = PTR_ERR(df); df = NULL; goto out; } + ksize = i_size_read(file_inode(kf)); + rsize = i_size_read(file_inode(rf)); + dsize = i_size_read(file_inode(df)); + ret = -EINVAL; + if (ksize < sizeof(kh) || !rsize || rsize > SZ_2M || dsize < sizeof(dh) || dsize > SZ_2M) + goto out; + ret = qkx_read(kf, kh, sizeof(kh), &pos); + if (ret) + goto out; + image_size = get_unaligned_le64(kh + 16); + flags = get_unaligned_le64(kh + 24); + ret = -EINVAL; + if (memcmp(kh + 56, "ARM\x64", 4) || get_unaligned_le64(kh + 8) != SZ_512K || + image_size < ksize || image_size > QKX_INITRD - QKX_ENTRY || + (flags & 1) || !(flags & 8) || ((flags & 6) != 0 && (flags & 6) != 2)) + goto out; + pos = 0; + ret = qkx_read(df, dh, sizeof(dh), &pos); + if (ret) + goto out; + ret = -EINVAL; + if (get_unaligned_be32(dh) != 0xd00dfeed || get_unaligned_be32(dh + 4) != dsize) + goto out; + ret = qkx_stage(kf, QKX_ENTRY, ksize, PAGE_ALIGN(image_size)); + if (!ret) + ret = qkx_stage(rf, QKX_INITRD, rsize, PAGE_ALIGN(rsize)); + if (!ret) + ret = qkx_stage(df, QKX_DTB, dsize, PAGE_ALIGN(dsize)); + if (!ret) + ret = qkx_identity_map(); +out: + if (kf) filp_close(kf, NULL); + if (rf) filp_close(rf, NULL); + if (df) filp_close(df, NULL); + if (!ret) { + pr_info("quest_kexec: staged %u copy pages, %u allocations; entry=%llx dtb=%llx; execute=%u\n", + nr_copies, nr_owned, QKX_ENTRY, QKX_DTB, execute); + if (test_transition) + ret = qkx_test_transition(); + if (test_full_copy) + ret = qkx_test_full_copy(); + if (test_cpus) + ret = qkx_test_cpus(); +#ifdef QKX_ENABLE_EXPERIMENTAL_JUMP + if (execute) { + qkx_phase(0, "entering execute path; resolving shutdown symbols", true); + ret = qkx_execute(); + } +#endif + } + if (ret) { + pr_err("quest_kexec: staging/validation failed: %d\n", ret); + qkx_release(); + } + return ret; +} + +static void __exit qkx_exit(void) +{ + qkx_release(); + pr_info("quest_kexec: unloaded; staging memory released\n"); +} +module_init(qkx_init); +module_exit(qkx_exit); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Experimental Quest Pro ARM64 kexec staging and EL1 transition"); diff --git a/module/marker_read.c b/module/marker_read.c new file mode 100644 index 0000000..57d0f7c --- /dev/null +++ b/module/marker_read.c @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Read-only diagnostic for the Quest kexec log in unused reserved ramoops RAM. */ +#include +#include +#include +#include +#include + +#define QKX_LOG_PHYS 0x9ba80000ULL +#define QKX_LOG_SIZE 0x10000 +#define QKX_LOG_MAGIC 0x514b584c +#define QKX_LOG_PAGES (QKX_LOG_SIZE / PAGE_SIZE) + +struct qkx_log_header { + u32 magic; + u32 write_pos; + u32 wraps; + u32 reserved; +}; + +static bool write_test; +module_param(write_test, bool, 0); +/* Invalidate a stale log before a run so it can't be mistaken for a new one. */ +static bool clear; +module_param(clear, bool, 0); + +static int __init qkx_marker_read_init(void) +{ + struct page *pages[QKX_LOG_PAGES]; + struct qkx_log_header *h; + char *ordered; + void *base; + u32 capacity = QKX_LOG_SIZE - sizeof(*h); + u32 pos, wraps, used, first, out = 0; + unsigned int i; + + for (i = 0; i < QKX_LOG_PAGES; i++) + pages[i] = pfn_to_page((QKX_LOG_PHYS >> PAGE_SHIFT) + i); + base = vmap(pages, QKX_LOG_PAGES, VM_MAP | VM_IOREMAP, + pgprot_writecombine(PAGE_KERNEL)); + if (!base) + return -ENOMEM; + h = base; + if (clear) { + WRITE_ONCE(h->magic, 0); + wmb(); + pr_emerg("qkx_marker_read: cleared retained log\n"); + vunmap(base); + return 0; + } + if (write_test) { + static const char test[] = "QKX_WARM_RESET_RETENTION_TEST_v1\n"; + + memset(base, 0, QKX_LOG_SIZE); + memcpy(base + sizeof(*h), test, sizeof(test) - 1); + WRITE_ONCE(h->write_pos, sizeof(test) - 1); + WRITE_ONCE(h->wraps, 0); + WRITE_ONCE(h->magic, QKX_LOG_MAGIC); + wmb(); + pr_emerg("qkx_marker_read: wrote warm-reset retention test\n"); + } + if (READ_ONCE(h->magic) != QKX_LOG_MAGIC) { + pr_emerg("qkx_marker_read: no retained log; magic=%08x\n", + READ_ONCE(h->magic)); + vunmap(base); + return 0; + } + pos = min_t(u32, READ_ONCE(h->write_pos), capacity - 1); + wraps = READ_ONCE(h->wraps); + used = wraps ? capacity : pos; + first = wraps ? pos : 0; + ordered = kmalloc(used + 1, GFP_KERNEL); + if (!ordered) { + vunmap(base); + return -ENOMEM; + } + if (used) { + u32 tail = min(used, capacity - first); + memcpy(ordered, base + sizeof(*h) + first, tail); + memcpy(ordered + tail, base + sizeof(*h), used - tail); + } + ordered[used] = '\0'; + vunmap(base); + + pr_emerg("qkx_marker_read: retained log bytes=%u wraps=%u pos=%u\n", + used, wraps, pos); + while (out < used) { + u32 len = 0; + + while (out + len < used && len < 220 && ordered[out + len] != '\n') + len++; + pr_emerg("qkxlog: %.*s\n", (int)len, ordered + out); + out += len; + if (out < used && ordered[out] == '\n') + out++; + } + kfree(ordered); + return 0; +} + +static void __exit qkx_marker_read_exit(void) +{ +} + +module_init(qkx_marker_read_init); +module_exit(qkx_marker_read_exit); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Read retained Quest kexec log from reserved RAM"); diff --git a/module/transition.S b/module/transition.S new file mode 100644 index 0000000..0bb0578 --- /dev/null +++ b/module/transition.S @@ -0,0 +1,181 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* Quest Pro: EL1, 4K pages, 39-bit VA. No stack use after TTBR0 switch. + * The relocation loop follows Linux arm64's relocate_kernel.S cache rules. + */ +#include +#include +#include + +.text +.align 4 +/* x0: private identity PGD, x1: physical control page, x2: descriptor head, + * x3: Image entry, x4: DTB. The high module mapping remains in TTBR1. + */ +ENTRY(qkx_enter) + msr daifset, #0xf + /* First invalidate the old TTBR0 translations with walks disabled. */ + mrs x9, tcr_el1 + orr x9, x9, #(1 << 7) + msr tcr_el1, x9 + isb + dsb ishst + tlbi vmalle1 + dsb ish + isb + msr ttbr0_el1, x0 + /* T0SZ=25, TG0=4K; preserve cacheability/shareability and TTBR1. */ + bic x9, x9, #0x3f + mov x10, #25 + orr x9, x9, x10 + bic x9, x9, #(3 << 14) + bic x9, x9, #(1 << 37) + bic x9, x9, #(1 << 7) + msr tcr_el1, x9 + isb + br x1 +ENDPROC(qkx_enter) + +/* Copied to a separately allocated physical page. No external references. */ +.align 4 +ENTRY(qkx_reloc_start) + mov x17, x3 + mov x18, x4 + mov x19, x2 + mrs x9, sctlr_el1 + ldr x10, =SCTLR_ELx_FLAGS + bic x9, x9, x10 + pre_disable_mmu_workaround + msr sctlr_el1, x9 + isb + raw_dcache_line_size x15, x9 +1: + cbz x19, 5f + /* Descriptor pages: { next, count, { source, destination }[255] }. */ + ldp x20, x21, [x19] + add x19, x19, #16 +2: + cbz x21, 4f + ldp x22, x23, [x19], #16 + mov x9, x23 + add x10, x23, #4096 +3: + dc ivac, x9 + add x9, x9, x15 + cmp x9, x10 + b.lo 3b + dsb sy + /* PAGE_SIZE bytes, both addresses page-aligned and nonoverlapping. */ + mov x11, #256 +6: + ldp x9, x10, [x22], #16 + stp x9, x10, [x23], #16 + subs x11, x11, #1 + b.ne 6b + sub x21, x21, #1 + b 2b +4: + mov x19, x20 + b 1b +5: + dsb sy + ic iallu + dsb sy + isb + mov x0, x18 + mov x1, xzr + mov x2, xzr + mov x3, xzr + br x17 + .ltorg +qkx_reloc_end: +ENDPROC(qkx_reloc_start) +.global qkx_reloc_end + +/* Reversible test of the very same TTBR0/physical/MMU-off transition. + * x0..x4 are passed through to the physical code. The relocation loop uses + * x17..x23, so preserve every callee-saved register and keep the translation + * state in x24..x27 until the original SCTLR is restored and control returns + * high. + */ +.align 4 +ENTRY(qkx_roundtrip) + stp x19, x20, [sp, #-96]! + stp x21, x22, [sp, #16] + stp x23, x24, [sp, #32] + stp x25, x26, [sp, #48] + stp x27, x28, [sp, #64] + str x30, [sp, #80] + mrs x24, tcr_el1 + mrs x25, ttbr0_el1 + mrs x26, sctlr_el1 + adr x27, .Lroundtrip_return + orr x9, x24, #(1 << 7) + msr tcr_el1, x9 + isb + dsb ishst + tlbi vmalle1 + dsb ish + isb + msr ttbr0_el1, x0 + bic x9, x9, #0x3f + mov x10, #25 + orr x9, x9, x10 + bic x9, x9, #(3 << 14) + bic x9, x9, #(1 << 37) + bic x9, x9, #(1 << 7) + msr tcr_el1, x9 + isb + br x1 +.Lroundtrip_return: + /* Disable TTBR0 walks before restoring the saved translation context. */ + mrs x9, tcr_el1 + orr x9, x9, #(1 << 7) + msr tcr_el1, x9 + isb + msr ttbr0_el1, x25 + dsb ishst + tlbi vmalle1 + dsb ish + isb + msr tcr_el1, x24 + isb + ldr x30, [sp, #80] + ldp x27, x28, [sp, #64] + ldp x25, x26, [sp, #48] + ldp x23, x24, [sp, #32] + ldp x21, x22, [sp, #16] + ldp x19, x20, [sp], #96 + ret +ENDPROC(qkx_roundtrip) + +.align 4 +ENTRY(qkx_probe_phys_start) + mrs x9, sctlr_el1 + ldr x10, =SCTLR_ELx_FLAGS + bic x9, x9, x10 + pre_disable_mmu_workaround + msr sctlr_el1, x9 + isb + mrs x0, sctlr_el1 + and x0, x0, x10 + /* Result zero proves the cleared flags were read back while physical. */ + msr sctlr_el1, x26 + isb + br x27 + .ltorg +.global qkx_probe_phys_end +qkx_probe_phys_end: +ENDPROC(qkx_probe_phys_start) + +/* Appended to the copied relocation code for its reversible scratch test. + * qkx_reloc_start leaves the completion value in x0, restores SCTLR here, + * then returns through the high mapping saved by qkx_roundtrip. + */ +.align 4 +ENTRY(qkx_reloc_return_start) + msr sctlr_el1, x26 + isb + br x27 +.global qkx_reloc_return_end +qkx_reloc_return_end: +ENDPROC(qkx_reloc_return_start) diff --git a/tools/capture.sh b/tools/capture.sh new file mode 100755 index 0000000..50dd43a --- /dev/null +++ b/tools/capture.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Capture the device-specific inputs needed by prepare.py (read-only on device). +# Usage: capture.sh +set -euo pipefail +O=$1; mkdir -p "$O" +su() { adb shell "su -c \"$1\""; } +# /sys/firmware/fdt is the original boot FDT. Kexec needs the kernel's +# runtime tree after overlays/fixups; using the boot FDT caused a cold reset. +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +adb exec-out 'su -c "tar -C /proc/device-tree -cf - ."' | + tar --warning=no-timestamp -C "$tmp" -xf - +dtc -q -I fs -O dtb "$tmp" -o "$O/runtime.dtb" +su 'cat /proc/iomem' > "$O/iomem" +su 'cat /proc/cmdline' > "$O/cmdline" +adb exec-out 'su -c "zcat /proc/config.gz"' > "$O/android.config" +adb shell uname -r > "$O/android-release" +echo "captured into $O: runtime.dtb iomem cmdline android.config android-release" diff --git a/tools/prep.sh b/tools/prep.sh new file mode 100755 index 0000000..4be935c --- /dev/null +++ b/tools/prep.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Build a payload directory from the captured inputs and a target Image. +# Usage: prep.sh [extra cmdline...] +set -euo pipefail +HERE=$(cd "$(dirname "$0")" && pwd) +C=$1 IMG=$2 RD=$3 OUT=$4; shift 4 +# Android's cmdline minus the boot-time watchdog bits, plus what the target needs. +BASE=$(sed -e 's/softdog.soft_panic=1//' "$C/cmdline") +python3 "$HERE/prepare.py" --live-dtb "$C/runtime.dtb" --iomem "$C/iomem" \ + --kernel-image "$IMG" --initramfs "$RD" --output "$OUT" \ + --cmdline "$BASE rdinit=/init nokaslr printk.devkmsg=on qkx_qmp_adopt=1 initcall_blacklist=virtual_sensor_driver_init $*" \ + > "$OUT.manifest.log" +echo "prepared $OUT" diff --git a/tools/prepare.py b/tools/prepare.py new file mode 100755 index 0000000..89dff4d --- /dev/null +++ b/tools/prepare.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Prepare Image + initramfs + DTB for the Quest Pro kexec staging layout. + +The Android boot image is optional; it is only needed when the kernel or +initramfs is taken from it. + +Only writes host files. Does not contact the headset or execute a boot. +""" +import argparse +import gzip +import hashlib +import json +import struct +import subprocess +import zlib +from pathlib import Path + + +def align(n, a): + return (n + a - 1) & -a + + +def unpack(data): + if len(data) < 1660 or data[:8] != b"ANDROID!": + raise ValueError("not an Android boot image") + ksize, _, rsize, _, ssize, _, _, page, version, _ = struct.unpack_from("<10I", data, 8) + if version not in (0, 1, 2): + raise ValueError("only boot header versions 0, 1 and 2 are supported") + if page not in (2048, 4096, 8192, 16384) or not ksize: + raise ValueError("invalid page or kernel size") + if ssize: + raise ValueError("second-stage bootloader payloads are not supported") + roff = align(page + ksize, page) + soff = align(roff + rsize, page) + if soff + ssize > len(data) or roff + rsize > len(data): + raise ValueError("truncated boot image") + kernel = data[page:page + ksize] + if kernel.startswith(b"\x1f\x8b"): + # Gzip streams with appended DTBs need separate handling, not guesses. + kernel = gzip.decompress(kernel) + if len(kernel) < 64 or kernel[56:60] != b"ARM\x64": + raise ValueError("kernel is not an uncompressed/gzip ARM64 Image") + text_offset, image_size, flags = struct.unpack_from(" 0x2F80000: + raise ValueError("image does not fit this Quest Pro staging layout") + initrd = a.initramfs.read_bytes() if a.initramfs else original + dtb = a.live_dtb.read_bytes() + if len(dtb) < 40 or struct.unpack_from(">I", dtb)[0] != 0xD00DFEED: + raise ValueError("invalid live DTB") + if len(initrd) > 0x200000 or len(dtb) > 0x200000: + raise ValueError("initramfs/DTB exceeds staging limits") + # Leave a single exclusion window for source, descriptor and control pages. + check_ram(a.iomem.read_text(), 0x90000000, 0x3400000) + a.output.mkdir(parents=True, exist_ok=True) + (a.output / "Image").write_bytes(kernel) + if original: + (a.output / "original-ramdisk.gz").write_bytes(original) + (a.output / "initramfs").write_bytes(initrd) + dtpath = a.output / "boot.dtb" + dtpath.write_bytes(dtb) + args = a.cmdline if a.cmdline is not None else report["cmdline"] + subprocess.run(["fdtput", "-t", "s", str(dtpath), "/chosen", "bootargs", args], check=True) + # The target kernel takes over the apps watchdog from the loader instead of + # probing it again; this matches every validated boot. + if not a.keep_watchdog: + subprocess.run(["fdtput", "-t", "s", str(dtpath), "/soc/qcom,wdt@17c10000", + "status", "disabled"], check=True) + for name, address in (("linux,initrd-start", 0x93000000), + ("linux,initrd-end", 0x93000000 + len(initrd))): + subprocess.run(["fdtput", "-t", "x", str(dtpath), "/chosen", name, + "0", f"{address:x}"], check=True) + report.update(kernel_address=0x90080000, initrd_address=0x93000000, + dtb_address=0x93200000, initrd_size=len(initrd), + dtb_size=dtpath.stat().st_size, effective_cmdline=args, + replacement_initramfs=a.initramfs is not None, + crc32={name: f"{zlib.crc32(content):08x}" for name, content in + (("Image", kernel), ("initramfs", initrd), ("boot.dtb", dtpath.read_bytes()))}, + status="prepared only; no boot attempted") + (a.output / "manifest.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tools/qkx_sh.py b/tools/qkx_sh.py new file mode 100755 index 0000000..06841d0 --- /dev/null +++ b/tools/qkx_sh.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Run a shell command on the kexec target via its ECM telnetd (10.42.0.2:23). + +Usage: qkx_sh.py 'COMMAND' [timeout_seconds] (env QKX_HOST overrides 10.42.0.2) +""" +import os +import socket +import sys +import time + + +def main(): + cmd = sys.argv[1] + limit = float(sys.argv[2]) if len(sys.argv) > 2 else 15 + marker = "QKX%dDONE" % os.getpid() + s = socket.create_connection((os.environ.get("QKX_HOST", "10.42.0.2"), 23), timeout=5) + s.settimeout(0.2) + # Split the marker in the command so only real output contains it whole. + s.sendall(('%s; echo QKX%d""DONE\n' % (cmd, os.getpid())).encode()) + buf = b"" + end = time.time() + limit + while time.time() < end and marker.encode() not in buf: + try: + chunk = s.recv(65536) + if not chunk: + break + buf += chunk + except socket.timeout: + pass + s.close() + out = buf.replace(b"\xff\xfb\x01", b"").replace(b"\xff\xfb\x03", b"") + sys.stdout.write(out.decode(errors="replace").split(marker)[0]) + + +if __name__ == "__main__": + main() diff --git a/tools/read-log.sh b/tools/read-log.sh new file mode 100755 index 0000000..e7bae1b --- /dev/null +++ b/tools/read-log.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# After the device is back in Android, print the log the target kernel kept +# in RAM across a warm reset, plus any Android pstore panic record. +set -uo pipefail +adb shell 'su -c "insmod /data/local/tmp/qkx_marker.ko; rmmod marker_read; dmesg"' | + awk '/qkx_marker_read:/{f=1} f' +adb shell 'su -c "cat /sys/fs/pstore/dmesg-ramoops-0 2>/dev/null"' | grep -a 'quest_kexec\|Kernel panic' | tail -5 diff --git a/tools/run.sh b/tools/run.sh new file mode 100755 index 0000000..e1bb88a --- /dev/null +++ b/tools/run.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Push a prepared payload plus the loader, verify hashes, and execute the kexec. +# Usage: run.sh [extra loader params...] +# Needs adb with a working `su`. Files go only to /data/local/tmp. +set -uo pipefail +HERE=$(cd "$(dirname "$0")/.." && pwd) +P=$1; shift +T=/data/local/tmp +MOD=$HERE/module/quest_kexec.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 + [ -f "$f" ] || { echo "missing $f"; exit 1; } +done +adb push "$MOD" $T/qkx.ko >/dev/null +adb push "$HERE/module/marker_read.ko" $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 "$P/boot.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" "$P/boot.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; } +done +# Invalidate the previous retained log so a reset can't reuse it. +adb shell "su -c 'insmod $T/qkx_marker.ko clear=1; rmmod marker_read'" >/dev/null 2>&1 +echo "verified; executing: $PARAMS" +adb shell "su -c 'insmod $T/qkx.ko image=$T/qkx-Image initrd=$T/qkx-initramfs dtb=$T/qkx-boot.dtb $PARAMS'" +sleep 2 +if adb shell 'su -c true' >/dev/null 2>&1; then + echo "loader returned without jumping:" + adb shell 'su -c "dmesg | grep quest_kexec | tail -5"' + adb shell 'su -c "rmmod quest_kexec"' >/dev/null 2>&1 + exit 1 +fi +echo "jumped. Expect USB gadget 1d6b:0105; then: telnet 10.42.0.2"