booting android

This commit is contained in:
YusufOzmen01 committed 2026-09-30 22:23:44 +03:00
1 parent 01996d8c3c
commit b7c14227e9
35 files changed
+2601 -12

No files matched your search

+46 -4
View File
@@ -1,5 +1,10 @@
#!/usr/bin/env python3
"""Pack a static ARM64 BusyBox and init.sh into a gzip newc initramfs."""
"""Pack a static ARM64 BusyBox and init.sh into a gzip newc initramfs.
With --os-dir the archive also carries everything needed to assemble a custom
Android from pinned images on userdata: the extent maps, the partition table and
the qkx-* helper scripts.
"""
import argparse
import gzip
import stat
@@ -38,8 +43,18 @@ def main():
ap.add_argument("--init", type=Path, default=Path(__file__).with_name("init.sh"))
ap.add_argument("--extra", action="append", default=[], metavar="SRC:DEST",
help="add a static ARM64 binary, e.g. modeset:bin/modeset")
ap.add_argument("--os-dir", type=Path,
help="directory from os-install.sh holding maps/ and partitions")
ap.add_argument("--os-scripts", type=Path,
default=Path(__file__).with_name("os"),
help="directory of qkx-* target scripts")
ap.add_argument("--qkx-dm", type=Path,
help="static qkx_dm binary, required with --os-dir")
ap.add_argument("--max-size", type=lambda s: int(s, 0), default=0x200000)
a = ap.parse_args()
dirs = ("bin", "sbin", "dev", "dev/pts", "proc", "sys", "config", "etc", "root", "tmp")
dirs = ["bin", "sbin", "dev", "dev/pts", "proc", "sys", "config", "etc", "root", "tmp"]
if a.os_dir:
dirs += ["etc/qkx", "etc/qkx/maps", "android"]
entries = [(d, stat.S_IFDIR | 0o755, b"", 0, 0) for d in dirs]
entries += [
("bin/busybox", stat.S_IFREG | 0o755, check_static_arm64(a.busybox), 0, 0),
@@ -50,13 +65,40 @@ def main():
("etc/passwd", stat.S_IFREG | 0o644, b"root::0:0:root:/root:/bin/sh\n", 0, 0),
("etc/group", stat.S_IFREG | 0o644, b"root:x:0:\n", 0, 0),
]
# The kernel-log watcher is useful even without the OS payload.
for script in sorted(a.os_scripts.glob("qkx-*")):
if not a.os_dir and script.name != "qkx-klogwatch":
continue
entries.append((f"bin/{script.name}", stat.S_IFREG | 0o755,
script.read_bytes(), 0, 0))
if a.os_dir:
if not a.qkx_dm:
raise SystemExit("--os-dir requires --qkx-dm")
entries.append(("bin/qkx_dm", stat.S_IFREG | 0o755,
check_static_arm64(a.qkx_dm), 0, 0))
partitions = a.os_dir / "partitions"
if not partitions.is_file():
raise SystemExit(f"{partitions}: missing; rerun tools/os-install.sh")
entries.append(("etc/qkx/partitions", stat.S_IFREG | 0o644,
partitions.read_bytes(), 0, 0))
maps = sorted((a.os_dir / "maps").glob("*.map"))
if not maps:
raise SystemExit(f"{a.os_dir}/maps: no extent maps found")
for m in maps:
if not m.read_text().strip():
raise SystemExit(f"{m}: empty extent map")
entries.append((f"etc/qkx/maps/{m.name}", stat.S_IFREG | 0o644,
m.read_bytes(), 0, 0))
print(f"OS payload: {len(maps)} maps "
f"({', '.join(m.stem for m in maps)})")
for spec in a.extra:
src, dest = spec.split(":", 1)
entries.append((dest, stat.S_IFREG | 0o755, check_static_arm64(Path(src)), 0, 0))
entries.append(("TRAILER!!!", 0, b"", 0, 0))
data = gzip.compress(archive(entries), mtime=0)
if len(data) > 0x200000:
raise SystemExit(f"initramfs is {len(data)} bytes; the staging layout allows 2 MiB")
if len(data) > a.max_size:
raise SystemExit(f"initramfs is {len(data)} bytes; the staging layout "
f"allows {a.max_size}")
a.output.parent.mkdir(parents=True, exist_ok=True)
a.output.write_bytes(data)
print(f"{a.output}: {len(data)} bytes")
+31 -5
View File
@@ -1,7 +1,10 @@
#!/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.
# Init for the kexec target: USB Ethernet (ECM) gadget with a BusyBox telnet
# shell. The headset is 10.42.0.2; udhcpd hands the host 10.42.0.1.
#
# If /etc/qkx/maps is present this also assembles a custom Android under
# /android, but never starts it: it leaves a shell so a broken boot stays
# debuggable. Run qkx-launch-android when ready.
export PATH=/bin:/sbin
export HOME=/root
export TERM=vt100
@@ -68,8 +71,31 @@ option lease 86400
EOC
touch /tmp/udhcpd.leases
udhcpd /tmp/udhcpd.conf
telnetd -l /bin/sh -p 23
# Respawn telnetd: the new OS shares our process table and has killed it before.
(while :; do telnetd -F -l /bin/sh -p 23; log "telnetd exited ($?); restarting"; sleep 1; done) &
echo $! > /tmp/telnet.pid
bootdone d
# Keep a full kernel log on hand: the shell arrives long after early boot, and
# BusyBox dmesg cannot follow the log by itself.
qkx-klogwatch -f /tmp/kernel.log >/dev/null 2>&1 &
if [ -d /etc/qkx/maps ]; then
log 'assembling custom OS'
# Keep the transcript: the shell only arrives after this has run. Avoid a
# pipeline so the exit status is qkx-mount-os's own.
qkx-mount-os >/tmp/mount-os.log 2>&1
status=$?
cat /tmp/mount-os.log
if [ $status -eq 0 ]; then
log 'custom OS mounted at /android; run qkx-launch-android to start it'
else
log "qkx-mount-os failed (status $status); see /tmp/mount-os.log"
fi
fi
log "ready: telnet 10.42.0.2"
while :; do sleep 3600; done
# Only PID 1 may switch_root, so qkx-launch-android asks this loop to do it.
while [ ! -e /tmp/qkx-launch ]; do sleep 1; done
exec qkx-switch
+28
View File
@@ -0,0 +1,28 @@
#!/bin/busybox sh
# Bind-mount edited copies of files from the read-only OS images.
#
# Meta's init scripts live on images we mount read-only, so edits go into
# /data/qkx/overrides mirroring the absolute path, e.g.
# /data/qkx/overrides/vendor/etc/init/hw/init.seacliff.rc
# replaces /vendor/etc/init/hw/init.seacliff.rc for the next launch.
#
# Use qkx-edit <path> to seed a file from the original.
set -e
ROOT=${1:-/android}
OVR="$ROOT/data/qkx/overrides"
log() { echo "qkx-override: $*"; }
[ -d "$OVR" ] || exit 0
find "$OVR" -type f | while read -r src; do
rel=${src#"$OVR"}
dst="$ROOT$rel"
if [ ! -e "$dst" ]; then
log "skipping $rel (no such file in the OS)"
continue
fi
mount --bind "$src" "$dst"
log "applied $rel"
done
+30
View File
@@ -0,0 +1,30 @@
#!/bin/busybox sh
# Seed an override from the OS image so it can be edited, e.g.
# qkx-edit /vendor/etc/init/hw/init.seacliff.rc
# vi /android/data/qkx/overrides/vendor/etc/init/hw/init.seacliff.rc
#
# Overrides live on the writable data image, so they survive reboots and are
# applied by qkx-apply-overrides on the next mount.
set -e
ROOT=/android
REL=${1:?usage: qkx-edit <absolute-path-inside-the-new-OS>}
case "$REL" in
/*) ;;
*) echo "qkx-edit: path must be absolute"; exit 1 ;;
esac
SRC="$ROOT$REL"
DST="$ROOT/data/qkx/overrides$REL"
[ -f "$SRC" ] || { echo "qkx-edit: $REL is not a file in the mounted OS"; exit 1; }
mkdir -p "${DST%/*}"
if [ -e "$DST" ]; then
echo "qkx-edit: $DST already exists; leaving it alone"
else
cp "$SRC" "$DST"
echo "qkx-edit: copied $REL"
fi
echo "edit: $DST"
echo "then: qkx-apply-overrides (or reboot the target) before qkx-launch-android"
+48
View File
@@ -0,0 +1,48 @@
#!/bin/busybox sh
# Follow the kernel log, which BusyBox's dmesg cannot do (it has no -w).
#
# qkx-klogwatch follow, showing the buffered log first
# qkx-klogwatch -n follow only messages newer than this moment
# qkx-klogwatch -g PATTERN keep only matching lines
# qkx-klogwatch -f FILE also append everything to FILE
#
# /dev/kmsg yields one record per read as "<prio>,<seq>,<usec>,<flags>;<text>"
# and keeps blocking for new records, which is what makes following possible.
CUTOFF=-1
PATTERN=
OUTFILE=
while [ $# -gt 0 ]; do
case $1 in
-n)
# Drop whatever is already buffered by taking the newest buffered
# record's timestamp as the cutoff.
CUTOFF=$(dmesg | sed -n 's/^\[ *\([0-9][0-9]*\.[0-9]*\)\].*/\1/p' |
tail -1)
[ -n "$CUTOFF" ] || CUTOFF=-1
;;
-g) PATTERN=$2; shift ;;
-f) OUTFILE=$2; shift ;;
-h|--help) sed -n '3,8p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "qkx-klogwatch: unknown option $1" >&2; exit 1 ;;
esac
shift
done
# cat blocks on /dev/kmsg once the buffer is drained, so this follows the log.
cat /dev/kmsg | awk -v cutoff="$CUTOFF" -v pat="$PATTERN" '
{
sep = index($0, ";")
if (sep) {
split(substr($0, 1, sep - 1), f, ",")
stamp = f[3] / 1000000
text = substr($0, sep + 1)
} else {
stamp = 0
text = $0
}
if (stamp <= cutoff) next
if (pat != "" && text !~ pat) next
printf "[%12.6f] %s\n", stamp, text
fflush()
}' | { [ -n "$OUTFILE" ] && tee -a "$OUTFILE" || cat; }
+72
View File
@@ -0,0 +1,72 @@
#!/bin/busybox sh
# Start the mounted Android in one of two ways.
#
# qkx-launch-android switch_root (default): the system image becomes
# / and Android init runs as PID 1, as on a normal
# boot. PID 1 does the switch (see qkx-switch);
# telnet and the log watcher are restarted inside
# the new root, so reconnect after a few seconds.
#
# qkx-launch-android chroot init in a chroot beside this shell. Kept for
# experiments; second stage expects to be PID 1
# on the real root and does not get far.
#
# Android's own first stage is skipped because it would insist on AVB, logical
# partitions and Keymaster-backed encryption. Entering at selinux_setup keeps the
# rest of the normal sequence: it loads sepolicy and then execs second_stage.
set -e
ROOT=/android
MODE=${1:-switch}
log() { echo "qkx-launch: $*"; }
[ -x "$ROOT/system/bin/init" ] || { log 'nothing mounted; run qkx-mount-os first'; exit 1; }
case "$MODE" in
chroot)
# This kernel has no PID namespaces (CONFIG_PID_NS off, as on stock
# Android), so init runs as an ordinary process beside our shell.
log 'starting init in a chroot; output in /tmp/android-init.log'
setsid chroot "$ROOT" /system/bin/init selinux_setup \
</dev/null >/tmp/android-init.log 2>&1 &
log "init pid $!"
;;
switch)
# The loader cleanly shuts down stock ADSP before kexec. Acquire it again
# through the normal subsystem/PAS API before Android class-main starts;
# otherwise adsprpcd starts with no RPMSG transport and never triggers PIL.
if [ -f /bin/qkx-adsp-hold.ko ] && ! grep -q '^adsp_hold ' /proc/modules; then
# Android init normally mounts modem_a at vendor/firmware_mnt, but
# that is too late for this pre-init request. Use the pinned, verified
# copy qkx-mount-os already reconstructed at /dev/qkx/modemfw instead
# of the real partition -- modem_a itself is never opened here.
if [ -b /dev/qkx/modemfw ]; then
mkdir -p /tmp/qkx-fw
mount -o ro -t vfat /dev/qkx/modemfw /tmp/qkx-fw
echo -n /tmp/qkx-fw/image > /sys/module/firmware_class/parameters/path
log 'starting ADSP through subsystem_get'
insmod /bin/qkx-adsp-hold.ko
echo -n '' > /sys/module/firmware_class/parameters/path
umount /tmp/qkx-fw
else
log 'no pinned modemfw image (run tools/os-install-modemfw.sh); skipping early ADSP boot'
fi
fi
# Stand in for the bootloader's continuous splash: bind the panel
# pipeline and drop DRM master before Android's composer opens the card,
# so the composer becomes master and Oculus's runtime finds the panel
# bound, as on a normal boot.
if [ -x /bin/qkx-splash ] && [ -e /dev/dri/card0 ]; then
setsid /bin/qkx-splash 180 </dev/null >/dev/kmsg 2>&1 &
sleep 2
fi
log 'asking PID 1 to switch root; reconnect telnet in a few seconds'
sync
touch /tmp/qkx-launch
;;
*)
echo "usage: qkx-launch-android [chroot|switch]"
exit 1
;;
esac
+129
View File
@@ -0,0 +1,129 @@
#!/bin/busybox sh
# Assemble the custom Android OS under /android, without touching the stock OS.
#
# The OS images live in pinned files on userdata, whose raw blocks we stitch back
# together with dm-linear. Device-specific partitions (persist, modem firmware,
# spunvm) are left to Android's own fstab so they come from the real hardware.
#
# /metadata is deliberately a tmpfs: the real one holds the stock OS's
# encryption keys, and the new OS must never write there.
set -e
ROOT=/android
CFG=/etc/qkx
IMAGES="system system_ext vendor odm product data"
# modemfw is a pinned, verified copy of modem_a (holds the ADSP/CDSP firmware
# images); reconstructed here like the others but left unmounted until
# qkx-launch-android needs it, so the real modem_a partition is never touched.
[ -s "$CFG/maps/modemfw.map" ] && IMAGES="$IMAGES modemfw"
log() { echo "qkx-mount: $*"; }
byname() {
awk -v n="$1" '$1 == n { print $2; found = 1 } END { exit !found }' \
"$CFG/partitions"
}
# The partition map records Android's paths, but here only devtmpfs exists, which
# uses /dev/<name> and has no /dev/block hierarchy or by-name symlinks. Fall back
# to creating the node from sysfs so the name in the map is all we rely on.
resolve_dev() {
base=${1##*/}
for p in "/dev/$base" "/dev/block/$base"; do
[ -b "$p" ] && { echo "$p"; return 0; }
done
if [ -r "/sys/class/block/$base/dev" ]; then
mm=$(cat "/sys/class/block/$base/dev")
mkdir -p /dev/block
if mknod "/dev/block/$base" b "${mm%:*}" "${mm#*:}"; then
echo "/dev/block/$base"
return 0
fi
fi
return 1
}
USERDATA=$(byname userdata) || { log 'no userdata in partition map'; exit 1; }
USERDATA=$(resolve_dev "$USERDATA") || { log 'cannot find the userdata device'; exit 1; }
if [ -e "$ROOT/system/bin/init" ]; then
log 'already mounted'
exit 0
fi
# device-mapper's control node is not created by devtmpfs.
if [ ! -e /dev/mapper/control ]; then
mkdir -p /dev/mapper
mknod /dev/mapper/control c 10 \
"$(awk '$2 == "device-mapper" { print $1 }' /proc/misc)"
fi
log "userdata is $USERDATA"
mkdir -p /dev/qkx "$ROOT"
for name in $IMAGES; do
[ -s "$CFG/maps/$name.map" ] || { log "no map for $name"; exit 1; }
if [ -e "/dev/qkx/$name" ]; then
continue
fi
set -- $(qkx_dm create "qkx-$name" "$USERDATA" "$CFG/maps/$name.map")
mknod "/dev/qkx/$name" b "$1" "$2"
log "$name: $(( $3 / 1024 / 1024 )) MiB via dm-linear"
done
# system.img is the root filesystem; everything else mounts inside it.
log 'mounting root (system)'
mount -o ro -t ext4 /dev/qkx/system "$ROOT"
[ -e "$ROOT/system/bin/init" ] || { log 'system image has no /system/bin/init'; exit 1; }
for name in system_ext vendor odm product; do
mkdir -p "$ROOT/$name" 2>/dev/null || true
log "mounting $name"
mount -o ro -t ext4 "/dev/qkx/$name" "$ROOT/$name"
done
log 'mounting data'
mount -o rw,noatime -t ext4 /dev/qkx/data "$ROOT/data"
mkdir -p "$ROOT/data/qkx/overrides"
# Pseudo filesystems that Android's first stage would normally have set up.
log 'mounting pseudo filesystems'
mount -t tmpfs -o mode=0755,nosuid tmpfs "$ROOT/dev"
mkdir -p "$ROOT/dev/pts" "$ROOT/dev/socket"
mount -t devpts devpts "$ROOT/dev/pts"
mount -t proc proc "$ROOT/proc"
mount -t sysfs sysfs "$ROOT/sys"
[ -d "$ROOT/sys/fs/selinux" ] && mount -t selinuxfs selinuxfs "$ROOT/sys/fs/selinux"
mount -t tmpfs -o mode=0755 tmpfs "$ROOT/metadata"
# First-stage init normally puts these writable tmpfs mounts over the read-only
# system root. We skip first stage, and second stage aborts in
# SetupMountNamespaces without them.
for d in linkerconfig apex debug_ramdisk second_stage_resources; do
[ -d "$ROOT/$d" ] && mount -t tmpfs -o mode=0755,nodev,nosuid tmpfs "$ROOT/$d"
done
mount -t tmpfs -o mode=0755,uid=0,gid=1000,nodev,noexec,nosuid tmpfs "$ROOT/mnt"
mkdir -p "$ROOT/mnt/vendor" "$ROOT/mnt/product"
# ueventd repopulates /dev, but init and the linker need these immediately.
mknod "$ROOT/dev/null" c 1 3 && chmod 666 "$ROOT/dev/null"
mknod "$ROOT/dev/zero" c 1 5 && chmod 666 "$ROOT/dev/zero"
mknod "$ROOT/dev/random" c 1 8 && chmod 666 "$ROOT/dev/random"
mknod "$ROOT/dev/urandom" c 1 9 && chmod 666 "$ROOT/dev/urandom"
mknod "$ROOT/dev/kmsg" c 1 11
mknod "$ROOT/dev/console" c 5 1
mknod "$ROOT/dev/ptmx" c 5 2 && chmod 666 "$ROOT/dev/ptmx"
# Android's /dev is this tmpfs, not the initramfs devtmpfs: give it the
# device-mapper node (ueventd and apexd race for it during coldboot) and the
# image nodes the patched fstab points at.
mknod "$ROOT/dev/device-mapper" c 10 236 && chmod 600 "$ROOT/dev/device-mapper"
mkdir -p "$ROOT/dev/qkx"
for n in /dev/qkx/*; do
set -- $(stat -c '%t %T' "$n")
mknod "$ROOT$n" b $((0x$1)) $((0x$2))
done
qkx-patch-fstab "$ROOT"
qkx-patch-usb "$ROOT"
qkx-apply-overrides "$ROOT"
log 'ready; run qkx-launch-android to start init'
+42
View File
@@ -0,0 +1,42 @@
#!/bin/busybox sh
# Rewrite the vendor fstab for a pre-mounted, unencrypted boot.
#
# The stock fstab expects logical partitions verified by AVB and an encrypted
# /data unlocked through Keymaster. We already mounted those from images, and we
# cannot reproduce the encryption, so those entries are dropped or relaxed.
#
# Device-specific entries (persist, modem firmware, spunvm, usbotg) are kept
# untouched so the new OS mounts them from the real hardware.
#
# The rewritten file is bind-mounted over the read-only original.
set -e
ROOT=${1:-/android}
HW=$(sed -n 's/.*androidboot.hardware=\([^ ]*\).*/\1/p' /proc/cmdline)
[ -n "$HW" ] || HW=seacliff
SRC="$ROOT/vendor/etc/fstab.$HW"
NEW="/tmp/fstab.$HW"
log() { echo "qkx-fstab: $*"; }
[ -f "$SRC" ] || { log "no $SRC"; exit 1; }
# Drop the entries we pre-mount, and /metadata so the new OS keeps to its tmpfs
# instead of writing to the stock OS's key storage.
awk '
$2 == "/system" || $2 == "/system_ext" || $2 == "/vendor" ||
$2 == "/odm" || $2 == "/product" || $2 == "/metadata" {
print "# qkx: pre-mounted or withheld -- " $0
next
}
$2 == "/data" {
print "# qkx: replaced -- " $0
print "/dev/qkx/data /data ext4 noatime,nosuid,nodev wait,nofail"
next
}
{ print }
' "$SRC" > "$NEW"
mount --bind "$NEW" "$SRC"
log "patched $SRC"
grep -E '^(/dev|[a-z])' "$SRC" | awk '{ printf " %-22s %s\n", $2, $1 }'
+66
View File
@@ -0,0 +1,66 @@
#!/bin/busybox sh
# Keep the new OS away from the real USB gadget.
#
# Android configures USB through configfs from its rc files, adbd property
# triggers and the USB HAL binary; binding its gadget replaced our ECM gadget and
# cut off the debug shell. Rather than chase every writer, the new OS gets a
# plain tmpfs at /config: its gadget writes succeed there but reach no hardware,
# and our qkx gadget in the real configfs stays bound.
#
# Rewritten rc files are bind-mounted over the read-only originals.
set -e
ROOT=${1:-/android}
log() { echo "qkx-usb: $*"; }
n=0
for rc in "$ROOT"/system/etc/init/hw/*.rc "$ROOT"/vendor/etc/init/hw/*.rc \
"$ROOT"/vendor/etc/init/*.rc "$ROOT"/odm/etc/init/*.rc; do
[ -f "$rc" ] || continue
grep -q 'mount configfs' "$rc" || continue
new=/tmp/rc.$n.${rc##*/}
sed 's/^\([[:space:]]*\)\(mount configfs .*\)$/\1# qkx: tmpfs instead -- \2/' \
"$rc" > "$new"
mount --bind "$new" "$rc"
log "configfs mount disabled in ${rc#$ROOT}"
n=$((n + 1))
done
[ $n -gt 0 ] || log 'warning: no configfs mount found in the rc files'
mountpoint -q "$ROOT/config" || mount -t tmpfs -o mode=0755,nodev,noexec,nosuid qkxcfg "$ROOT/config"
mkdir -p "$ROOT/config/usb_gadget" "$ROOT/config/sdcardfs"
log "sandbox configfs on ${ROOT}/config"
# Suspects for the power/USB cutoff seen after launch: keep them from starting
# by hiding their rc files. Override with QKX_KEEP_RC=1 on the kernel cmdline.
if ! grep -q 'QKX_KEEP_RC=1' /proc/cmdline; then
: > /tmp/empty.rc
QKX_EXTRA_HIDE=$(sed -n "s/.*qkx_hide=\([^ ]*\).*/\1/p" /proc/cmdline | tr , " ")
for rc in vendor/etc/init/hvdcp_opti.rc odm/etc/init/charging_accessory_service.rc \
odm/etc/init/android.hardware.usb-service.rc \
system_ext/etc/init/init.trackingmisc.rc \
system_ext/etc/init/init.vision.rc \
$QKX_EXTRA_HIDE; do
[ -f "$ROOT/$rc" ] || continue
mount --bind /tmp/empty.rc "$ROOT/$rc"
log "disabled /$rc"
done
fi
# Android-managed fallback logger. The executable is staged in /dev/.qkx by
# qkx-switch before second-stage init starts.
cat > /tmp/qkx-log.rc <<'EOF'
service qkx_klog /dev/.qkx/bin/qkx-logrun
class core
user root
group root
disabled
on post-fs-data
start qkx_klog
EOF
if [ -f "$ROOT/system/etc/init/dmesgd.rc" ]; then
mount --bind /tmp/qkx-log.rc "$ROOT/system/etc/init/dmesgd.rc"
log 'installed Android-managed persistent logger'
fi
BIN
View File
Binary file not shown.
+114
View File
@@ -0,0 +1,114 @@
#!/bin/busybox sh
# Run by PID 1 (init.sh) when qkx-launch-android asks for it: make the system
# image the real root and hand PID 1 to Android's init, as on a normal boot.
#
# The initramfs is emptied by switch_root, so the debug services are restarted
# from a BusyBox copy on the new root's /dev tmpfs (second-stage init leaves /dev
# mounted) and chrooted there, which keeps them working after the root moves.
ROOT=/android
Q=/dev/.qkx
log() { echo "qkx-switch: $*"; }
mkdir -p "$ROOT$Q/bin"
cp /bin/busybox "$ROOT$Q/busybox"
chroot "$ROOT" $Q/busybox --install -s $Q/bin
cp /bin/qkx-klogwatch "$ROOT$Q/bin/"
cat > "$ROOT$Q/bin/qkx-logrun" <<'EOF'
#!/dev/.qkx/busybox sh
mkdir -p /data/qkx
# /dev/kmsg readers begin at the oldest record. Drop the already captured boot
# backlog so a slow single-CPU run records the impending failure, not 2 MiB of
# historical messages. The independent retained-RAM console is unaffected.
/dev/.qkx/busybox dmesg -c >/dev/null
: > /data/qkx/kernel-current.raw
echo "qkx logger started $(date)" >> /data/qkx/kernel-current.raw
/dev/.qkx/busybox cat /dev/kmsg >> /data/qkx/kernel-current.raw &
while :; do
/dev/.qkx/busybox sync
/dev/.qkx/busybox sleep 2
done
EOF
cat > "$ROOT$Q/bin/qkx-waitlog" <<'EOF'
#!/dev/.qkx/busybox sh
# Started after the staging mount is gone; wait for Android's mount_all.
while ! awk '$2 == "/data" { found=1 } END { exit !found }' /proc/mounts; do
/dev/.qkx/busybox sleep 1
done
exec /dev/.qkx/bin/qkx-logrun
EOF
chmod 755 "$ROOT$Q/bin/qkx-logrun" "$ROOT$Q/bin/qkx-waitlog"
# Command helpers for the new root, e.g. returning to stock Android.
cat > "$ROOT$Q/profile" <<EOF
export PATH=$Q/bin:/system/bin:/vendor/bin
export ENV=$Q/profile
PS1='qkx-android# '
EOF
# Replace the initramfs telnetd and log watcher with chrooted copies.
kill $(cat /tmp/telnet.pid) 2>/dev/null
pkill -f qkx-klogwatch 2>/dev/null
pkill telnetd 2>/dev/null
sleep 1
chroot "$ROOT" $Q/busybox env PATH=$Q/bin ENV=$Q/profile \
$Q/busybox sh -c "while :; do telnetd -F -l $Q/bin/sh -p 23; sleep 1; done" \
</dev/null >/dev/null 2>&1 &
chroot "$ROOT" $Q/busybox env PATH=$Q/bin \
$Q/bin/qkx-klogwatch -f $Q/kernel.log </dev/null >/dev/null 2>&1 &
# Stream /dev/kmsg to the host over UDP (tools/klog-udp.py). Needs neither
# /data nor a live TCP session, so it covers the window before Android's disk
# logger starts. A restarted reader replays the backlog; the host dedups by
# sequence number.
chroot "$ROOT" $Q/busybox sh -c \
"while :; do $Q/busybox cat /dev/kmsg | $Q/busybox nc -u 10.42.0.1 5140; $Q/busybox sleep 1; done" \
</dev/null >/dev/null 2>&1 &
# netd replaces the routing policy with fwmark rules that send everything not
# on an Android-managed network to an unreachable table, which silently cuts
# the rescue ECM link about 70 ms after it starts. Keep a higher-priority rule
# for the rescue subnet; netd may flush it, so re-add it every second.
cat > "$ROOT$Q/bin/qkx-keepnet" <<'EOF'
#!/dev/.qkx/busybox sh
# Re-assert every piece of the rescue usb0 path each second: Android's netd
# may take the link down, drop the address, reorder policy rules or add
# firewall chains. Each step is idempotent. Log the first time each repair is
# needed so the persistent log shows what Android changed.
B=/dev/.qkx/busybox
IPT=/system/bin/iptables
note() { grep -q "$1" /dev/.qkx/keepnet.seen 2>/dev/null ||
{ echo "$1" >> /dev/.qkx/keepnet.seen; echo "qkx-keepnet: repaired $1" > /dev/kmsg; }; }
while :; do
$B ip link show usb0 | $B grep -q ',UP' || { $B ip link set usb0 up; note link; }
$B ip -4 addr show usb0 | $B grep -q 10.42.0.2 ||
{ $B ip addr add 10.42.0.2/24 dev usb0; note addr; }
$B ip route show table main | $B grep -q '10.42.0.0/24' ||
{ $B ip route add 10.42.0.0/24 dev usb0 table main; note route; }
for r in 'from 10.42.0.0/24' 'to 10.42.0.0/24'; do
$B ip rule show | $B grep -q "$r lookup main" ||
{ $B ip rule add $r lookup main pref 50; note "rule $r"; }
done
if [ -x $IPT ]; then
$IPT -w -C INPUT -i usb0 -j ACCEPT 2>/dev/null ||
{ $IPT -w -I INPUT 1 -i usb0 -j ACCEPT 2>/dev/null && note ipt-in; }
$IPT -w -C OUTPUT -o usb0 -j ACCEPT 2>/dev/null ||
{ $IPT -w -I OUTPUT 1 -o usb0 -j ACCEPT 2>/dev/null && note ipt-out; }
fi
$B sleep 1
done
EOF
chmod 755 "$ROOT$Q/bin/qkx-keepnet"
chroot "$ROOT" $Q/bin/qkx-keepnet </dev/null >/dev/null 2>&1 &
# qkx-mount-os mounts data so the rescue shell can inspect it. Android's
# mount_all must mount it itself: leaving it mounted makes fs_mgr run e2fsck on
# a live filesystem and then continue after EBUSY. The retained kernel ring is
# used for post-mortem logging during system-as-root runs.
sync
if ! umount "$ROOT/data"; then
log 'refusing switch: could not unmount staged data image'
exit 1
fi
# Android starts qkx-logrun from the generated post-fs-data rc.
# udhcpd keeps running from the old root; it only needs its open files.
log 'switching root to the system image; Android init takes PID 1'
cd /
exec switch_root "$ROOT" /system/bin/init selinux_setup
+186
View File
@@ -0,0 +1,186 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Read-only list of the physical ranges stock ION has handed to secure VMs:
* live buffers carrying ION_FLAG_SECURE or a CP flag, and the pages parked in
* the system heap's secure pools. Those pages stay hyp-assigned across kexec,
* and touching one from the next kernel hangs the CPU.
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/rbtree.h>
#include <linux/scatterlist.h>
#include <linux/mm.h>
#include <linux/msm_ion.h>
#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion.h"
#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion_system_heap.h"
#include <linux/kprobes.h>
#include "kgsl_device.h"
/* Neither is exported; resolved at load time through a kprobe address. */
static struct kgsl_driver *kgsl_drv;
static unsigned long lookup(const char *name)
{
static unsigned long (*kln)(const char *);
if (!kln) {
struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
if (register_kprobe(&kp))
return 0;
kln = (void *)kp.addr;
unregister_kprobe(&kp);
}
return kln(name);
}
#define SECURE_FLAGS (ION_FLAG_SECURE | GENMASK(30, 17))
static phys_addr_t run_start, run_end;
static unsigned long total;
static void emit(phys_addr_t start, size_t len)
{
total += len;
if (run_end == start) {
run_end += len;
return;
}
if (run_end)
pr_info("ionsec: %pa-%pa\n", &run_start, &run_end);
run_start = start;
run_end = start + len;
}
static void dump_pool(struct ion_page_pool *pool)
{
struct page *page;
mutex_lock(&pool->mutex);
list_for_each_entry(page, &pool->high_items, lru)
emit(page_to_phys(page), PAGE_SIZE << pool->order);
list_for_each_entry(page, &pool->low_items, lru)
emit(page_to_phys(page), PAGE_SIZE << pool->order);
mutex_unlock(&pool->mutex);
}
static void dump_buffer(struct ion_buffer *buf, const char *what)
{
struct scatterlist *sg;
int i;
if (!(buf->flags & SECURE_FLAGS) || !buf->sg_table)
return;
pr_info("ionsec: %s heap=%s flags=%#lx size=%zu\n", what,
buf->heap->name, buf->flags, buf->size);
for_each_sg(buf->sg_table->sgl, sg, buf->sg_table->nents, i)
emit(sg_phys(sg), sg->length);
}
static unsigned long sg_total(struct sg_table *sgt)
{
struct scatterlist *sg;
unsigned long n = 0;
int i;
for_each_sg(sgt->sgl, sg, sgt->nents, i) {
emit(sg_phys(sg), sg->length);
n += sg->length;
}
return n;
}
/* The GPU driver locks its secure buffers to VMID_CP_PIXEL on its own. */
static void dump_kgsl(void)
{
struct kgsl_process_private *private;
struct kgsl_global_memdesc *md;
struct kgsl_device *device;
struct kgsl_mem_entry *entry;
struct page **guard;
unsigned long found = 0;
int id;
kgsl_drv = (struct kgsl_driver *)lookup("kgsl_driver");
if (!kgsl_drv) {
pr_info("ionsec: kgsl_driver not found\n");
return;
}
device = kgsl_drv->devp[0];
guard = (struct page **)lookup("kgsl_secure_guard_page");
if (guard && *guard) {
emit(page_to_phys(*guard), PAGE_SIZE);
found += PAGE_SIZE;
}
if (device)
list_for_each_entry(md, &device->globals, node)
if ((md->memdesc.flags & KGSL_MEMFLAGS_SECURE) &&
md->memdesc.sgt)
found += sg_total(md->memdesc.sgt);
spin_lock(&kgsl_drv->proclist_lock);
list_for_each_entry(private, &kgsl_drv->process_list, list) {
spin_lock(&private->mem_lock);
idr_for_each_entry(&private->mem_idr, entry, id)
if ((entry->memdesc.flags & KGSL_MEMFLAGS_SECURE) &&
entry->memdesc.sgt)
found += sg_total(entry->memdesc.sgt);
spin_unlock(&private->mem_lock);
}
spin_unlock(&kgsl_drv->proclist_lock);
pr_info("ionsec: kgsl secure found %lu KiB, driver counts %ld KiB\n",
found >> 10, atomic_long_read(&kgsl_drv->stats.secure) >> 10);
}
static int __init ion_secmap_init(void)
{
struct ion_device *idev;
struct ion_heap *heap;
struct rb_node *n;
struct file *f;
int vmid, i;
f = filp_open("/dev/ion", O_RDONLY, 0);
if (IS_ERR(f))
return PTR_ERR(f);
idev = container_of((struct miscdevice *)f->private_data,
struct ion_device, dev);
pr_info("ionsec: begin\n");
mutex_lock(&idev->buffer_lock);
for (n = rb_first(&idev->buffers); n; n = rb_next(n))
dump_buffer(rb_entry(n, struct ion_buffer, node), "buffer");
mutex_unlock(&idev->buffer_lock);
down_read(&idev->lock);
plist_for_each_entry(heap, &idev->heaps, node) {
struct ion_system_heap *sys;
struct ion_buffer *buf;
/* Freed buffers wait here, still assigned, for the free thread. */
if (heap->flags & ION_HEAP_FLAG_DEFER_FREE) {
spin_lock(&heap->free_lock);
list_for_each_entry(buf, &heap->free_list, list)
dump_buffer(buf, "deferred");
spin_unlock(&heap->free_lock);
}
if (heap->type != ION_HEAP_TYPE_SYSTEM)
continue;
sys = container_of(heap, struct ion_system_heap, heap);
for (vmid = 0; vmid < VMID_LAST; vmid++)
for (i = 0; i < MAX_ORDER; i++)
if (sys->secure_pools[vmid][i])
dump_pool(sys->secure_pools[vmid][i]);
}
up_read(&idev->lock);
dump_kgsl();
emit(0, 0);
pr_info("ionsec: total %lu KiB\n", total >> 10);
filp_close(f, NULL);
return -EAGAIN;
}
module_init(ion_secmap_init);
MODULE_LICENSE("GPL");
Binary file not shown.
+68 -1
View File
@@ -10,6 +10,72 @@
#define QKX_LOG_SIZE 0x10000
#define QKX_LOG_MAGIC 0x514b584c
#define QKX_LOG_PAGES (QKX_LOG_SIZE / PAGE_SIZE)
/* Must match include/linux/qkx_diag.h in the diagnostic kernel. */
#define QKX_CTXAREA_SIZE 0x3000
#define QKX_CTXAREA_MAGIC 0x514b5843
struct qkx_ctx {
u64 fn;
s32 irq;
char what[48];
u64 since;
u64 arg0, arg1;
};
struct qkx_ctxarea {
u32 magic, nr_cpus, nr_kinds, rec_size;
u64 text, last_pet;
struct qkx_ctx rec[];
};
/* Print per-CPU "inside" records; fn is printed relative to runtime _text. */
static void qkx_dump_ctxarea(void *base)
{
static const char * const kinds[] = { "hardirq", "sysfs-attr(unused)", "scm", "clk", "gdsc", "spmi", "cache", "cpu-pm", "irq-mask", "ufs-mmio" };
struct qkx_ctxarea *a = base + QKX_LOG_SIZE - QKX_CTXAREA_SIZE;
u32 cpu, k, n = 0;
if (a->magic != QKX_CTXAREA_MAGIC || a->rec_size != sizeof(struct qkx_ctx) ||
a->nr_cpus > 64 || a->nr_kinds > 16 ||
sizeof(*a) + a->nr_cpus * a->nr_kinds * sizeof(struct qkx_ctx) > QKX_CTXAREA_SIZE) {
pr_emerg("qkxctx: no context area (magic=%08x rec=%u)\n", a->magic, a->rec_size);
return;
}
pr_emerg("qkxctx: text=%llx last_pet=%llu.%09llu\n", a->text,
a->last_pet / 1000000000ULL, a->last_pet % 1000000000ULL);
for (cpu = 0; cpu < a->nr_cpus; cpu++)
for (k = 0; k < a->nr_kinds; k++) {
struct qkx_ctx *c = &a->rec[cpu * a->nr_kinds + k];
if (!c->fn)
continue;
c->what[sizeof(c->what) - 1] = 0;
pr_emerg("qkxctx: cpu%u %s since=%llu.%09llu irq/arg=%d what=%s fn=_text+%llx arg=%llx/%llx\n",
cpu, k < ARRAY_SIZE(kinds) ? kinds[k] : "?",
c->since / 1000000000ULL, c->since % 1000000000ULL,
c->irq, c->what, c->fn - a->text, c->arg0, c->arg1);
n++;
}
pr_emerg("qkxctx: %u active records\n", n);
for (cpu = 0; cpu < 8; cpu++) {
u64 *h = (void *)a + 0x2800 + cpu * 22 * sizeof(u64);
u64 cnt = h[0];
if (!cnt)
continue;
/* oldest to newest; bit0 = user mode */
for (k = 0; k < 7 && k < cnt; k++) {
u64 slot = 1 + ((cnt - k) % 7) * 3;
u64 pc = h[slot], daif = h[slot + 1], gic = h[slot + 2];
pr_emerg("qkxpc: cpu%u tick=%llu %s %s%llx daif=%llx pmr=%x grp=%x\n",
cpu, cnt - k, (pc & 1) ? "user" : "kern",
(pc & 1) ? "" : "_text+",
(pc & 1) ? pc & ~1ULL : pc - a->text, daif,
(u32)gic, (u32)(gic >> 32));
}
}
}
struct qkx_log_header {
u32 magic;
@@ -30,7 +96,7 @@ static int __init qkx_marker_read_init(void)
struct qkx_log_header *h;
char *ordered;
void *base;
u32 capacity = QKX_LOG_SIZE - sizeof(*h);
u32 capacity = QKX_LOG_SIZE - QKX_CTXAREA_SIZE - sizeof(*h);
u32 pos, wraps, used, first, out = 0;
unsigned int i;
@@ -80,6 +146,7 @@ static int __init qkx_marker_read_init(void)
memcpy(ordered + tail, base + sizeof(*h), used - tail);
}
ordered[used] = '\0';
qkx_dump_ctxarea(base);
vunmap(base);
pr_emerg("qkx_marker_read: retained log bytes=%u wraps=%u pos=%u\n",
+215
View File
@@ -0,0 +1,215 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Last step before kexec: hand every secure ION/kgsl page the stock kernel
* still has assigned to another VM back to HLOS, using the stock kernel's own
* unassign routines (the same calls it makes when freeing those buffers).
* The stock kernel's bookkeeping is left stale on purpose; it is about to be
* replaced. Pages the hypervisor refuses to return are logged so they can
* still be reserved.
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/rbtree.h>
#include <linux/scatterlist.h>
#include <linux/mm.h>
#include <linux/msm_ion.h>
#include <linux/kprobes.h>
#include <soc/qcom/secure_buffer.h>
#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion.h"
#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion_system_heap.h"
#include "kgsl_device.h"
#define SECURE_FLAGS (ION_FLAG_SECURE | GENMASK(30, 17))
static int (*unassign_flags)(struct sg_table *sgt, unsigned long flags,
bool set_page_private);
static int (*assign_table)(struct sg_table *table, u32 *source_vm_list,
int source_nelems, int *dest_vmids,
int *dest_perms, int dest_nelems);
static unsigned long ok_bytes, failed_bytes;
static unsigned long lookup(const char *name)
{
static unsigned long (*kln)(const char *);
if (!kln) {
struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
if (register_kprobe(&kp))
return 0;
kln = (void *)kp.addr;
unregister_kprobe(&kp);
}
return kln(name);
}
static unsigned long sgt_bytes(struct sg_table *sgt)
{
struct scatterlist *sg;
unsigned long n = 0;
int i;
for_each_sg(sgt->sgl, sg, sgt->nents, i)
n += sg->length;
return n;
}
static void account(struct sg_table *sgt, int ret, const char *what)
{
struct scatterlist *sg;
int i;
if (!ret) {
ok_bytes += sgt_bytes(sgt);
return;
}
failed_bytes += sgt_bytes(sgt);
pr_info("giveback: %s failed %d\n", what, ret);
for_each_sg(sgt->sgl, sg, sgt->nents, i)
pr_info("giveback: kept 0x%llx-0x%llx\n", (u64)sg_phys(sg),
(u64)sg_phys(sg) + sg->length);
}
/* Return one sg_table from @source to HLOS with full permissions. */
static int to_hlos(struct sg_table *sgt, u32 source)
{
int dest = VMID_HLOS, perm = PERM_READ | PERM_WRITE | PERM_EXEC;
int ret;
do {
ret = assign_table(sgt, &source, 1, &dest, &perm, 1);
} while (ret == -EAGAIN);
return ret;
}
static void page_to_hlos(struct page *page, size_t len, u32 source)
{
struct scatterlist sgl;
struct sg_table sgt = { .sgl = &sgl, .nents = 1, .orig_nents = 1 };
sg_init_table(&sgl, 1);
sg_set_page(&sgl, page, len, 0);
account(&sgt, to_hlos(&sgt, source), "page");
}
static void ion_giveback(void)
{
struct ion_device *idev;
struct ion_heap *heap;
struct rb_node *n;
struct file *f;
int vmid, i;
f = filp_open("/dev/ion", O_RDONLY, 0);
if (IS_ERR(f)) {
pr_info("giveback: no /dev/ion\n");
return;
}
idev = container_of((struct miscdevice *)f->private_data,
struct ion_device, dev);
mutex_lock(&idev->buffer_lock);
for (n = rb_first(&idev->buffers); n; n = rb_next(n)) {
struct ion_buffer *buf = rb_entry(n, struct ion_buffer, node);
if ((buf->flags & SECURE_FLAGS) && buf->sg_table)
account(buf->sg_table,
unassign_flags(buf->sg_table, buf->flags, false),
buf->heap->name);
}
mutex_unlock(&idev->buffer_lock);
down_read(&idev->lock);
plist_for_each_entry(heap, &idev->heaps, node) {
struct ion_system_heap *sys;
struct ion_buffer *buf;
if (heap->flags & ION_HEAP_FLAG_DEFER_FREE) {
spin_lock(&heap->free_lock);
list_for_each_entry(buf, &heap->free_list, list)
if ((buf->flags & SECURE_FLAGS) && buf->sg_table)
account(buf->sg_table,
unassign_flags(buf->sg_table,
buf->flags, false),
"deferred");
spin_unlock(&heap->free_lock);
}
if (heap->type != ION_HEAP_TYPE_SYSTEM)
continue;
sys = container_of(heap, struct ion_system_heap, heap);
/* Pool pages sit assigned to the VM the pool is named after. */
for (vmid = 0; vmid < VMID_LAST; vmid++)
for (i = 0; i < MAX_ORDER; i++) {
struct ion_page_pool *pool =
sys->secure_pools[vmid][i];
struct page *page;
if (!pool)
continue;
mutex_lock(&pool->mutex);
list_for_each_entry(page, &pool->high_items, lru)
page_to_hlos(page,
PAGE_SIZE << pool->order, vmid);
list_for_each_entry(page, &pool->low_items, lru)
page_to_hlos(page,
PAGE_SIZE << pool->order, vmid);
mutex_unlock(&pool->mutex);
}
}
up_read(&idev->lock);
filp_close(f, NULL);
}
/* kgsl locks its secure memory to VMID_CP_PIXEL alone (kgsl_sharedmem.c). */
static void kgsl_giveback(void)
{
struct kgsl_driver *drv = (void *)lookup("kgsl_driver");
struct page **guard = (void *)lookup("kgsl_secure_guard_page");
struct kgsl_process_private *private;
struct kgsl_global_memdesc *md;
struct kgsl_mem_entry *entry;
int id;
if (!drv) {
pr_info("giveback: no kgsl_driver\n");
return;
}
if (guard && *guard)
page_to_hlos(*guard, PAGE_SIZE, VMID_CP_PIXEL);
if (drv->devp[0])
list_for_each_entry(md, &drv->devp[0]->globals, node)
if ((md->memdesc.flags & KGSL_MEMFLAGS_SECURE) &&
md->memdesc.sgt)
account(md->memdesc.sgt,
to_hlos(md->memdesc.sgt, VMID_CP_PIXEL),
"kgsl global");
/* hyp_assign sleeps, so collect under the spinlocks is not possible;
* userspace is stopped, so the lists no longer change. */
list_for_each_entry(private, &drv->process_list, list)
idr_for_each_entry(&private->mem_idr, entry, id)
if ((entry->memdesc.flags & KGSL_MEMFLAGS_SECURE) &&
entry->memdesc.sgt)
account(entry->memdesc.sgt,
to_hlos(entry->memdesc.sgt,
VMID_CP_PIXEL),
"kgsl entry");
}
static int __init giveback_init(void)
{
unassign_flags = (void *)lookup("ion_hyp_unassign_sg_from_flags");
assign_table = (void *)lookup("hyp_assign_table");
if (!unassign_flags || !assign_table)
return -ENOENT;
pr_info("giveback: begin\n");
ion_giveback();
kgsl_giveback();
pr_info("giveback: returned %lu KiB, kept %lu KiB\n",
ok_bytes >> 10, failed_bytes >> 10);
return -EAGAIN;
}
module_init(giveback_init);
MODULE_LICENSE("GPL");
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
# Static ARM64 helpers: qkx_fsmap and qkx_rawcp run under Android, qkx_dm runs
# in the kexec target's initramfs.
CROSS_COMPILE ?= aarch64-linux-gnu-
CC := $(CROSS_COMPILE)gcc
STRIP := $(CROSS_COMPILE)strip
CFLAGS ?= -static -O2 -Wall -Wextra -Wno-sign-compare
HELPERS := qkx_fsmap qkx_rawcp qkx_dm
all: $(HELPERS)
$(HELPERS): %: %.c
$(CC) $(CFLAGS) $< -o $@
$(STRIP) $@
clean:
rm -f $(HELPERS)
.PHONY: all clean
+96
View File
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Read-only list of the physical ranges stock ION has handed to secure VMs:
* live buffers carrying ION_FLAG_SECURE or a CP flag, and the pages parked in
* the system heap's secure pools. Those pages stay hyp-assigned across kexec,
* and touching one from the next kernel hangs the CPU.
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/rbtree.h>
#include <linux/scatterlist.h>
#include <linux/mm.h>
#include <linux/msm_ion.h>
#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion.h"
#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion_system_heap.h"
#define SECURE_FLAGS (ION_FLAG_SECURE | GENMASK(30, 17))
static phys_addr_t run_start, run_end;
static unsigned long total;
static void emit(phys_addr_t start, size_t len)
{
total += len;
if (run_end == start) {
run_end += len;
return;
}
if (run_end)
pr_info("ionsec: %pa-%pa\n", &run_start, &run_end);
run_start = start;
run_end = start + len;
}
static void dump_pool(struct ion_page_pool *pool)
{
struct page *page;
mutex_lock(&pool->mutex);
list_for_each_entry(page, &pool->high_items, lru)
emit(page_to_phys(page), PAGE_SIZE << pool->order);
list_for_each_entry(page, &pool->low_items, lru)
emit(page_to_phys(page), PAGE_SIZE << pool->order);
mutex_unlock(&pool->mutex);
}
static int __init ion_secmap_init(void)
{
struct ion_device *idev;
struct ion_heap *heap;
struct rb_node *n;
struct file *f;
int vmid, i;
f = filp_open("/dev/ion", O_RDONLY, 0);
if (IS_ERR(f))
return PTR_ERR(f);
idev = container_of((struct miscdevice *)f->private_data,
struct ion_device, dev);
mutex_lock(&idev->buffer_lock);
for (n = rb_first(&idev->buffers); n; n = rb_next(n)) {
struct ion_buffer *buf = rb_entry(n, struct ion_buffer, node);
struct scatterlist *sg;
if (!(buf->flags & SECURE_FLAGS) || !buf->sg_table)
continue;
pr_info("ionsec: buffer heap=%s flags=%#lx size=%zu\n",
buf->heap->name, buf->flags, buf->size);
for_each_sg(buf->sg_table->sgl, sg, buf->sg_table->nents, i)
emit(sg_phys(sg), sg->length);
}
mutex_unlock(&idev->buffer_lock);
down_read(&idev->lock);
plist_for_each_entry(heap, &idev->heaps, node) {
struct ion_system_heap *sys;
if (heap->type != ION_HEAP_TYPE_SYSTEM)
continue;
sys = container_of(heap, struct ion_system_heap, heap);
for (vmid = 0; vmid < VMID_LAST; vmid++)
for (i = 0; i < MAX_ORDER; i++)
if (sys->secure_pools[vmid][i])
dump_pool(sys->secure_pools[vmid][i]);
}
up_read(&idev->lock);
emit(0, 0);
pr_info("ionsec: total %lu KiB\n", total >> 10);
filp_close(f, NULL);
return -EAGAIN;
}
module_init(ion_secmap_init);
MODULE_LICENSE("GPL");
+114
View File
@@ -0,0 +1,114 @@
#!/system/bin/sh
# Device side of the custom-OS installer. Runs under Android as root.
#
# Images live as pinned f2fs files inside the work directory, but their contents
# are written straight to the userdata partition's blocks. Userdata is
# metadata-encrypted, so this is the only form the kexec target can read; from
# Android these files just look like garbage, which is expected.
#
# No partition is formatted and nothing outside these files is written.
set -e
W=/data/local/tmp/qkx
BIN=$W/bin
IMG=$W/img
MAP=$W/maps
DEV=/dev/block/sda7 # userdata; verified 1:1 under dm-default-key
usage() {
echo "usage: qkx-install.sh alloc <name> <bytes>"
echo " qkx-install.sh write <name> <staged-file>"
echo " qkx-install.sh writez <name> <staged-file.gz>"
echo " qkx-install.sh verify <name> <staged-file>"
echo " qkx-install.sh map <name>"
echo " qkx-install.sh list"
echo " qkx-install.sh partitions"
exit 1
}
need_map() {
[ -s "$MAP/$1.map" ] || { echo "qkx-install: no map for $1"; exit 1; }
}
cmd_alloc() {
name=$1; bytes=$2
mkdir -p "$IMG" "$MAP"
# Reallocating would hand out different blocks, invalidating a map that a
# staged initramfs may already contain.
if [ -f "$IMG/$name.img" ]; then
echo "qkx-install: $name already allocated; remove $IMG/$name.img first"
exit 1
fi
"$BIN/qkx_fsmap" alloc "$IMG/$name.img" "$bytes"
"$BIN/qkx_fsmap" map "$IMG/$name.img" > "$MAP/$name.map"
echo "qkx-install: $name allocated, $(wc -l < "$MAP/$name.map") extents"
}
cmd_write() {
name=$1; srcfile=$2
need_map "$name"
# Zero first: the blocks hold stale data, and a sparse copy would otherwise
# leave garbage wherever the image has holes.
"$BIN/qkx_rawcp" zero "$MAP/$name.map" "$DEV"
"$BIN/qkx_rawcp" copy "$MAP/$name.map" "$DEV" "$srcfile"
sync
}
cmd_writez() {
name=$1; srcgz=$2
need_map "$name"
"$BIN/qkx_rawcp" zero "$MAP/$name.map" "$DEV"
gzip -dc "$srcgz" | "$BIN/qkx_rawcp" copy "$MAP/$name.map" "$DEV" -
sync
}
cmd_verify() {
name=$1; srcfile=$2
need_map "$name"
# The map must still match the file, or we verified the wrong blocks.
"$BIN/qkx_fsmap" map "$IMG/$name.img" > "$MAP/$name.map.now"
if ! cmp "$MAP/$name.map" "$MAP/$name.map.now" >/dev/null 2>&1; then
echo "qkx-install: $name moved on disk; extents changed"
exit 1
fi
rm -f "$MAP/$name.map.now"
case "$srcfile" in
*.gz) gzip -dc "$srcfile" | "$BIN/qkx_rawcp" verify "$MAP/$name.map" "$DEV" - ;;
*) "$BIN/qkx_rawcp" verify "$MAP/$name.map" "$DEV" "$srcfile" ;;
esac
}
cmd_map() {
need_map "$1"
"$BIN/qkx_fsmap" map "$IMG/$1.img"
}
cmd_partitions() {
# "<name> <device node>" for every partition, so the kexec target can find
# them without a vendor init to create by-name symlinks.
for l in /dev/block/by-name/*; do
[ -e "$l" ] || continue
echo "${l##*/} $(readlink -f "$l")"
done
}
cmd_list() {
for f in "$IMG"/*.img; do
[ -e "$f" ] || continue
n=${f##*/}; n=${n%.img}
echo "$n $(stat -c %s "$f") bytes, $(wc -l < "$MAP/$n.map") extents"
done
}
[ $# -ge 1 ] || usage
op=$1; shift
case "$op" in
alloc) [ $# -eq 2 ] || usage; cmd_alloc "$@" ;;
write) [ $# -eq 2 ] || usage; cmd_write "$@" ;;
writez) [ $# -eq 2 ] || usage; cmd_writez "$@" ;;
verify) [ $# -eq 2 ] || usage; cmd_verify "$@" ;;
map) [ $# -eq 1 ] || usage; cmd_map "$@" ;;
list) cmd_list ;;
partitions) cmd_partitions ;;
*) usage ;;
esac
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
# Report whether the target kernel actually re-established the shared-memory
# link to the remote processors after kexec.
#
# SMEM is the table every AP<->DSP transport is built on (glink, smp2p,
# subsystem restart, socinfo). Stock prints a full socinfo line from it; if our
# kernel prints dummy values, SMEM lost its probe race and the DSPs -- which are
# still running from the stock kernel -- have no working link to the new AP.
# Android's audio/sensor/fastrpc HALs are the first things to depend on it.
echo "=== socinfo (dummy values mean SMEM was missed) ==="
dmesg | grep -iE 'socinfo_print|SMEM is not initialized|SMEM_HW_SW_BUILD_ID|by SBL'
echo
echo "=== is the smem device bound to a driver? ==="
for d in /sys/devices/platform/soc/*smem* /sys/bus/platform/devices/*smem*; do
[ -e "$d" ] || continue
echo "$d -> driver: $(readlink -f "$d/driver" 2>/dev/null || echo NONE)"
done
echo
echo "=== platform devices still waiting on deferred probe ==="
cat /sys/kernel/debug/devices_deferred 2>/dev/null | head -40 ||
echo "(devices_deferred needs debugfs mounted)"
echo
echo "=== rpmsg / glink endpoints (empty means no DSP link) ==="
ls /sys/bus/rpmsg/devices 2>/dev/null | head -30 || echo "(no rpmsg bus)"
echo
echo "=== subsystem restart devices ==="
ls /dev/subsys_* 2>/dev/null || echo "(none)"
cat /sys/bus/msm_subsys/devices/*/name 2>/dev/null | head
echo
echo "=== remote processor state ==="
for s in /sys/bus/msm_subsys/devices/subsys*/; do
[ -d "$s" ] || continue
echo "$(cat "$s/name" 2>/dev/null): restart_level=$(cat "$s/restart_level" 2>/dev/null) state=$(cat "$s/state" 2>/dev/null)"
done
BIN
View File
Binary file not shown.
+191
View File
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: GPL-2.0
/*
* qkx_dm - build a dm-linear device from a pinned-file extent map.
*
* The kexec target cannot read Android's encrypted userdata filesystem, but the
* blocks of a pinned file are plain: we stitch its physical extents back into a
* contiguous block device, which then holds an image we can mount normally.
*
* qkx_dm create <name> <backing-dev> <map-file> map file lines: "<off> <phys> <len>"
* qkx_dm remove <name>
*
* create prints "<major> <minor> <bytes>" so the caller can mknod the node
* itself; devtmpfs naming for dm devices is not something to rely on here.
*
* Build: aarch64-linux-gnu-gcc -static -O2 qkx_dm.c -o qkx_dm
*/
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <unistd.h>
#include <linux/dm-ioctl.h>
#define DM_CONTROL "/dev/mapper/control"
#define BUF_SIZE (1 << 20)
static char buf[BUF_SIZE];
static int die(const char *what)
{
fprintf(stderr, "qkx_dm: %s: %s\n", what, strerror(errno));
return 1;
}
static struct dm_ioctl *init_hdr(const char *name)
{
struct dm_ioctl *io = (struct dm_ioctl *)buf;
memset(buf, 0, BUF_SIZE);
io->version[0] = 4;
io->version[1] = 0;
io->version[2] = 0;
io->data_size = BUF_SIZE;
io->data_start = sizeof(*io);
snprintf(io->name, sizeof(io->name), "%s", name);
return io;
}
static int dm_open(void)
{
int fd = open(DM_CONTROL, O_RDWR);
if (fd < 0) {
/* device-mapper's control node is not created by devtmpfs. */
if (errno == ENOENT)
fprintf(stderr, "qkx_dm: %s missing; create it with "
"mknod (major 10, see /proc/misc)\n", DM_CONTROL);
return -1;
}
return fd;
}
static int do_remove(int fd, const char *name)
{
struct dm_ioctl *io = init_hdr(name);
if (ioctl(fd, DM_DEV_REMOVE, io))
return die("DM_DEV_REMOVE");
return 0;
}
static int do_create(int fd, const char *name, const char *backing, const char *mapfile)
{
unsigned long long off, phys, len, sectors = 0;
struct dm_ioctl *io;
struct stat st;
unsigned targets = 0;
char line[256];
char *ptr;
FILE *f;
if (stat(backing, &st))
return die(backing);
f = fopen(mapfile, "r");
if (!f)
return die(mapfile);
io = init_hdr(name);
if (ioctl(fd, DM_DEV_CREATE, io))
return die("DM_DEV_CREATE");
io = init_hdr(name);
ptr = buf + sizeof(*io);
while (fgets(line, sizeof(line), f)) {
struct dm_target_spec *spec;
char params[128];
size_t plen;
if (line[0] == '#' || line[0] == '\n')
continue;
if (sscanf(line, "%llu %llu %llu", &off, &phys, &len) != 3) {
fprintf(stderr, "qkx_dm: bad map line: %s", line);
return 1;
}
if ((off | phys | len) & 511) {
fprintf(stderr, "qkx_dm: extent not sector aligned: %s", line);
return 1;
}
if (off / 512 != sectors) {
fprintf(stderr, "qkx_dm: map is not contiguous at %llu\n", off);
return 1;
}
spec = (struct dm_target_spec *)ptr;
spec->sector_start = sectors;
spec->length = len / 512;
spec->status = 0;
snprintf(spec->target_type, sizeof(spec->target_type), "linear");
snprintf(params, sizeof(params), "%u:%u %llu",
major(st.st_rdev), minor(st.st_rdev), phys / 512);
plen = strlen(params) + 1;
/* dm requires each target's parameters to be 8-byte aligned. */
plen = (plen + 7) & ~7UL;
memcpy(ptr + sizeof(*spec), params, strlen(params) + 1);
spec->next = sizeof(*spec) + plen;
ptr += spec->next;
sectors += len / 512;
targets++;
if ((size_t)(ptr - buf) > BUF_SIZE - 512) {
fprintf(stderr, "qkx_dm: map too large\n");
return 1;
}
}
fclose(f);
if (!targets) {
fprintf(stderr, "qkx_dm: empty map\n");
return 1;
}
io->target_count = targets;
io->data_size = ptr - buf;
if (ioctl(fd, DM_TABLE_LOAD, io)) {
die("DM_TABLE_LOAD");
goto fail;
}
io = init_hdr(name);
if (ioctl(fd, DM_DEV_SUSPEND, io)) {
die("DM_DEV_SUSPEND");
goto fail;
}
printf("%u %u %llu\n", major(io->dev), minor(io->dev), sectors * 512);
return 0;
fail:
do_remove(fd, name);
return 1;
}
int main(int argc, char **argv)
{
int fd, ret;
fd = dm_open();
if (fd < 0)
return 1;
if (argc == 5 && !strcmp(argv[1], "create"))
ret = do_create(fd, argv[2], argv[3], argv[4]);
else if (argc == 3 && !strcmp(argv[1], "remove"))
ret = do_remove(fd, argv[2]);
else {
fprintf(stderr, "usage: qkx_dm create <name> <backing-dev> <map-file>\n"
" qkx_dm remove <name>\n");
ret = 1;
}
close(fd);
return ret;
}
BIN
View File
Binary file not shown.
+178
View File
@@ -0,0 +1,178 @@
// SPDX-License-Identifier: GPL-2.0
/*
* qkx_fsmap - allocate GC-immune (pinned) files on f2fs and dump their extents.
*
* Android's userdata is metadata-encrypted by dm-default-key and file-encrypted
* by FBE, so a kexec'd kernel cannot read the filesystem at all. Instead we let
* f2fs allocate and pin a file, then address its raw blocks on the underlying
* partition directly. Pinning keeps f2fs garbage collection from relocating the
* blocks, so the physical map stays valid across reboots.
*
* alloc <path> <bytes> create, pin and fully allocate a file
* map <path> print "<file_offset> <phys_offset> <length>" in bytes
* mapdd <path> <bs> same map in <bs> units, for dd's skip/seek/count
*
* mapdd exists because Android's shell evaluates $(( )) in 32 bits, so byte
* offsets into a 230 GiB partition silently overflow if divided in script.
*
* Build: aarch64-linux-gnu-gcc -static -O2 qkx_fsmap.c -o qkx_fsmap
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <linux/fiemap.h>
#include <linux/fs.h>
#define F2FS_IOC_SET_PIN_FILE _IOW(0xf5, 13, uint32_t)
#define F2FS_IOC_GET_PIN_FILE _IOR(0xf5, 14, uint32_t)
#define F2FS_SUPER_MAGIC 0xf2f52010
#define MAX_EXTENTS 4096
static int die(const char *what)
{
fprintf(stderr, "qkx_fsmap: %s: %s\n", what, strerror(errno));
return 1;
}
static int do_alloc(const char *path, unsigned long long size)
{
struct statfs sfs;
uint32_t pin = 1;
int fd;
fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (fd < 0)
return die("open");
if (fstatfs(fd, &sfs))
return die("fstatfs");
if (sfs.f_type != F2FS_SUPER_MAGIC) {
fprintf(stderr, "qkx_fsmap: %s is not on f2fs (magic %#lx); "
"pinning is unavailable\n", path, (unsigned long)sfs.f_type);
return 1;
}
/* Pin before allocating: f2fs only guarantees GC immunity for blocks
* that were allocated while the inode was already pinned. */
if (ioctl(fd, F2FS_IOC_SET_PIN_FILE, &pin))
return die("F2FS_IOC_SET_PIN_FILE");
if (fallocate(fd, 0, 0, (off_t)size))
return die("fallocate");
if (fsync(fd))
return die("fsync");
/* GET_PIN_FILE reports the GC failure count for the pinned inode, so a
* nonzero value means f2fs could not keep the blocks in place. */
pin = 0;
if (!ioctl(fd, F2FS_IOC_GET_PIN_FILE, &pin) && pin) {
fprintf(stderr, "qkx_fsmap: %s reports %u pin/GC failures\n",
path, pin);
return 1;
}
close(fd);
return 0;
}
static int do_map(const char *path, unsigned long long unit)
{
char buf[sizeof(struct fiemap) + MAX_EXTENTS * sizeof(struct fiemap_extent)];
struct fiemap *fm = (struct fiemap *)buf;
unsigned long long expect = 0;
struct stat st;
unsigned i;
int fd;
fd = open(path, O_RDONLY);
if (fd < 0)
return die("open");
if (fstat(fd, &st))
return die("fstat");
memset(buf, 0, sizeof(buf));
fm->fm_start = 0;
fm->fm_length = st.st_size;
fm->fm_flags = FIEMAP_FLAG_SYNC;
fm->fm_extent_count = MAX_EXTENTS;
if (ioctl(fd, FS_IOC_FIEMAP, fm))
return die("FS_IOC_FIEMAP");
for (i = 0; i < fm->fm_mapped_extents; i++) {
struct fiemap_extent *e = &fm->fm_extents[i];
/* Anything unwritten, inline or delalloc has no stable physical
* location, so refuse rather than hand back a bogus map.
*
* ENCODED|DATA_ENCRYPTED is expected and fine: under FBE the file's
* contents are encrypted but its block placement is not affected,
* and we address those blocks directly instead of via the
* filesystem. NOT_ALIGNED is rejected because we need block-exact
* offsets. */
if (e->fe_flags & (FIEMAP_EXTENT_UNKNOWN | FIEMAP_EXTENT_DELALLOC |
FIEMAP_EXTENT_DATA_INLINE | FIEMAP_EXTENT_DATA_TAIL |
FIEMAP_EXTENT_NOT_ALIGNED |
FIEMAP_EXTENT_UNWRITTEN)) {
fprintf(stderr, "qkx_fsmap: extent %u has unusable flags %#x\n",
i, e->fe_flags);
return 1;
}
if (e->fe_logical != expect) {
fprintf(stderr, "qkx_fsmap: hole before extent %u "
"(logical %llu, expected %llu)\n", i,
(unsigned long long)e->fe_logical, expect);
return 1;
}
expect = e->fe_logical + e->fe_length;
if ((e->fe_logical | e->fe_physical | e->fe_length) % unit) {
fprintf(stderr, "qkx_fsmap: extent %u is not a multiple "
"of %llu\n", i, unit);
return 1;
}
printf("%llu %llu %llu\n",
(unsigned long long)e->fe_logical / unit,
(unsigned long long)e->fe_physical / unit,
(unsigned long long)e->fe_length / unit);
}
if (expect < (unsigned long long)st.st_size) {
fprintf(stderr, "qkx_fsmap: mapped %llu of %llu bytes; "
"increase MAX_EXTENTS\n", expect,
(unsigned long long)st.st_size);
return 1;
}
close(fd);
return 0;
}
int main(int argc, char **argv)
{
if (argc == 4 && !strcmp(argv[1], "alloc"))
return do_alloc(argv[2], strtoull(argv[3], NULL, 0));
if (argc == 3 && !strcmp(argv[1], "map"))
return do_map(argv[2], 1);
if (argc == 4 && !strcmp(argv[1], "mapdd")) {
unsigned long long unit = strtoull(argv[3], NULL, 0);
if (!unit) {
fprintf(stderr, "qkx_fsmap: block size must be nonzero\n");
return 1;
}
return do_map(argv[2], unit);
}
fprintf(stderr, "usage: qkx_fsmap alloc <path> <bytes>\n"
" qkx_fsmap map <path>\n"
" qkx_fsmap mapdd <path> <blocksize>\n");
return 1;
}
BIN
View File
Binary file not shown.
+242
View File
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: GPL-2.0
/*
* qkx_rawcp - copy an image into a pinned file's raw blocks.
*
* Userdata is metadata-encrypted, so writing through the filesystem produces
* ciphertext that the kexec target cannot read. We instead write the image
* straight to the partition blocks that f2fs allocated to the pinned file, which
* the target reads back verbatim.
*
* qkx_rawcp zero <map> <dev> write zeros over every extent
* qkx_rawcp copy <map> <dev> <src>|- copy image (sparse: skips zero blocks)
* qkx_rawcp verify <map> <dev> <src>|- compare image against the raw blocks
*
* The map is qkx_fsmap's byte-unit output: "<file_offset> <phys_offset> <length>".
* Writing only ever touches those extents, never the surrounding filesystem.
*
* Build: aarch64-linux-gnu-gcc -static -O2 qkx_rawcp.c -o qkx_rawcp
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define CHUNK (1u << 20)
#define BLOCK 4096
enum mode { MODE_ZERO, MODE_COPY, MODE_VERIFY };
struct extent {
unsigned long long file;
unsigned long long phys;
unsigned long long len;
};
static unsigned char buf[CHUNK];
static unsigned char cmp[CHUNK];
static struct extent *ext;
static unsigned n_ext;
static int die(const char *what)
{
fprintf(stderr, "qkx_rawcp: %s: %s\n", what, strerror(errno));
return 1;
}
static int load_map(const char *path, unsigned long long *total)
{
unsigned long long expect = 0;
char line[256];
unsigned cap = 64;
FILE *f = fopen(path, "r");
if (!f)
return die(path);
ext = malloc(cap * sizeof(*ext));
if (!ext)
return die("malloc");
while (fgets(line, sizeof(line), f)) {
struct extent e;
if (line[0] == '#' || line[0] == '\n')
continue;
if (sscanf(line, "%llu %llu %llu", &e.file, &e.phys, &e.len) != 3) {
fprintf(stderr, "qkx_rawcp: bad map line: %s", line);
return 1;
}
if (e.file != expect) {
fprintf(stderr, "qkx_rawcp: map has a hole at %llu\n", e.file);
return 1;
}
if (e.len % BLOCK || e.phys % BLOCK) {
fprintf(stderr, "qkx_rawcp: extent is not %u-aligned\n", BLOCK);
return 1;
}
expect = e.file + e.len;
if (n_ext == cap) {
cap *= 2;
ext = realloc(ext, cap * sizeof(*ext));
if (!ext)
return die("realloc");
}
ext[n_ext++] = e;
}
fclose(f);
if (!n_ext) {
fprintf(stderr, "qkx_rawcp: empty map\n");
return 1;
}
*total = expect;
return 0;
}
static int is_zero(const unsigned char *p, size_t n)
{
size_t i;
for (i = 0; i < n; i++)
if (p[i])
return 0;
return 1;
}
/* Pipes return short reads; the image must be consumed in exact steps. */
static size_t read_full(int fd, unsigned char *p, size_t n)
{
size_t done = 0;
while (done < n) {
ssize_t r = read(fd, p + done, n - done);
if (r < 0) {
if (errno == EINTR)
continue;
return done;
}
if (!r)
break;
done += r;
}
return done;
}
static int write_at(int fd, const unsigned char *p, size_t n, unsigned long long off)
{
while (n) {
ssize_t w = pwrite(fd, p, n, (off_t)off);
if (w < 0) {
if (errno == EINTR)
continue;
return die("pwrite");
}
p += w;
n -= w;
off += w;
}
return 0;
}
int main(int argc, char **argv)
{
unsigned long long total, done = 0, written = 0, srcleft = ~0ULL;
int devfd, srcfd = -1, mode;
unsigned long long mismatch = 0;
const char *src = NULL;
unsigned i;
if (argc >= 4 && !strcmp(argv[1], "zero"))
mode = MODE_ZERO;
else if (argc == 5 && !strcmp(argv[1], "copy"))
mode = MODE_COPY;
else if (argc == 5 && !strcmp(argv[1], "verify"))
mode = MODE_VERIFY;
else {
fprintf(stderr, "usage: qkx_rawcp zero <map> <dev>\n"
" qkx_rawcp copy <map> <dev> <src>|-\n"
" qkx_rawcp verify <map> <dev> <src>|-\n");
return 1;
}
if (load_map(argv[2], &total))
return 1;
devfd = open(argv[3], mode == MODE_VERIFY ? O_RDONLY : O_RDWR);
if (devfd < 0)
return die(argv[3]);
if (mode != MODE_ZERO) {
src = argv[4];
srcfd = !strcmp(src, "-") ? STDIN_FILENO : open(src, O_RDONLY);
if (srcfd < 0)
return die(src);
} else {
memset(buf, 0, CHUNK);
}
for (i = 0; i < n_ext; i++) {
unsigned long long off = 0;
while (off < ext[i].len) {
size_t n = CHUNK;
size_t got;
if (ext[i].len - off < n)
n = ext[i].len - off;
if (mode == MODE_ZERO) {
if (write_at(devfd, buf, n, ext[i].phys + off))
return 1;
written += n;
off += n;
done += n;
continue;
}
got = srcleft ? read_full(srcfd, buf, n) : 0;
if (got < n) {
/* Short image: the rest of the file stays as the
* zeros written by the preceding zero pass. */
srcleft = 0;
memset(buf + got, 0, n - got);
}
if (mode == MODE_VERIFY) {
if (pread(devfd, cmp, n, (off_t)(ext[i].phys + off)) != (ssize_t)n)
return die("pread");
if (memcmp(buf, cmp, n))
mismatch += n;
} else if (!is_zero(buf, n)) {
if (write_at(devfd, buf, n, ext[i].phys + off))
return 1;
written += n;
}
off += n;
done += n;
if (!(done % (256ULL << 20)))
fprintf(stderr, "qkx_rawcp: %llu MiB / %llu MiB\n",
done >> 20, total >> 20);
}
}
if (mode != MODE_VERIFY && fsync(devfd))
return die("fsync");
close(devfd);
if (mode == MODE_VERIFY) {
if (mismatch) {
fprintf(stderr, "qkx_rawcp: %llu bytes differ\n", mismatch);
return 1;
}
fprintf(stderr, "qkx_rawcp: verified %llu MiB\n", done >> 20);
return 0;
}
fprintf(stderr, "qkx_rawcp: covered %llu MiB, wrote %llu MiB\n",
done >> 20, written >> 20);
return 0;
}
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Restore the original node order of a captured runtime device tree.
capture.sh rebuilds the runtime tree (with the bootloader's overlays applied)
from a copy of /proc/device-tree using `dtc -I fs`. dtc reads that copy in the
host filesystem's directory order, which is effectively a hash, so every
node's children end up shuffled. Order is not cosmetic on this kernel:
* /cpus order sets the logical CPU numbers (CPU1 became the prime core),
which breaks Android's cluster assumptions, thermal cooling-device names
and CPU affinities;
* drivers such as kgsl search forward from their own node for siblings.
This keeps the runtime tree's content and reorders children and properties
to follow the boot FDT (/sys/firmware/fdt). Nodes or properties that exist
only in the runtime tree keep their relative order after the known ones.
Usage: dt-reorder.py <runtime.dtb> <boot.fdt> <out.dtb>
"""
import struct
import sys
FDT_MAGIC = 0xd00dfeed
BEGIN_NODE, END_NODE, PROP, NOP, END = 1, 2, 3, 4, 9
class Node:
def __init__(self, name):
self.name = name
self.props = [] # [(name, bytes)]
self.children = [] # [Node]
def parse(blob):
(magic, totalsize, off_struct, off_strings, off_rsvmap, version,
last_comp, boot_cpuid, size_strings, size_struct) = struct.unpack_from('>10I', blob, 0)
if magic != FDT_MAGIC:
raise SystemExit('not an FDT')
strings = blob[off_strings:off_strings + size_strings]
def string_at(off):
return strings[off:strings.index(b'\0', off)].decode()
rsv = []
p = off_rsvmap
while True:
addr, size = struct.unpack_from('>QQ', blob, p)
p += 16
if addr == 0 and size == 0:
break
rsv.append((addr, size))
p = off_struct
stack = []
root = None
while True:
tok, = struct.unpack_from('>I', blob, p)
p += 4
if tok == BEGIN_NODE:
end = blob.index(b'\0', p)
node = Node(blob[p:end].decode())
p = (end + 1 + 3) & ~3
if stack:
stack[-1].children.append(node)
else:
root = node
stack.append(node)
elif tok == END_NODE:
stack.pop()
elif tok == PROP:
length, nameoff = struct.unpack_from('>II', blob, p)
p += 8
stack[-1].props.append((string_at(nameoff), blob[p:p + length]))
p = (p + length + 3) & ~3
elif tok == NOP:
continue
elif tok == END:
break
else:
raise SystemExit(f'bad token {tok} at {p - 4}')
return root, rsv, boot_cpuid, last_comp
def reorder(node, ref):
"""Order node's props/children like ref's; recurse into matching children."""
if ref is None:
for c in node.children:
reorder(c, None)
return
prop_rank = {n: i for i, (n, _) in enumerate(ref.props)}
node.props.sort(key=lambda pv: prop_rank.get(pv[0], len(prop_rank)))
child_rank = {c.name: i for i, c in enumerate(ref.children)}
ref_by_name = {c.name: c for c in ref.children}
# sort() is stable, so unknown children keep their relative order.
node.children.sort(key=lambda c: child_rank.get(c.name, len(child_rank)))
for c in node.children:
reorder(c, ref_by_name.get(c.name))
def serialize(root, rsv, boot_cpuid, last_comp):
strings = bytearray()
string_off = {}
def sref(name):
if name not in string_off:
string_off[name] = len(strings)
strings.extend(name.encode() + b'\0')
return string_off[name]
st = bytearray()
def pad():
while len(st) % 4:
st.append(0)
def emit(node):
st.extend(struct.pack('>I', BEGIN_NODE))
st.extend(node.name.encode() + b'\0')
pad()
for name, val in node.props:
st.extend(struct.pack('>III', PROP, len(val), sref(name)))
st.extend(val)
pad()
for c in node.children:
emit(c)
st.extend(struct.pack('>I', END_NODE))
emit(root)
st.extend(struct.pack('>I', END))
rsvmap = b''.join(struct.pack('>QQ', a, s) for a, s in rsv) + struct.pack('>QQ', 0, 0)
off_rsvmap = 40
off_rsvmap = (off_rsvmap + 7) & ~7
off_struct = off_rsvmap + len(rsvmap)
off_strings = off_struct + len(st)
totalsize = off_strings + len(strings)
hdr = struct.pack('>10I', FDT_MAGIC, totalsize, off_struct, off_strings, off_rsvmap,
17, last_comp, boot_cpuid, len(strings), len(st))
out = bytearray(hdr)
out.extend(b'\0' * (off_rsvmap - len(out)))
out.extend(rsvmap)
out.extend(st)
out.extend(strings)
return bytes(out)
def count(node):
return 1 + sum(count(c) for c in node.children)
def main():
runtime, rsv, boot_cpuid, last_comp = parse(open(sys.argv[1], 'rb').read())
boot, _, _, _ = parse(open(sys.argv[2], 'rb').read())
before = count(runtime)
reorder(runtime, boot)
assert count(runtime) == before
open(sys.argv[3], 'wb').write(serialize(runtime, rsv, boot_cpuid, last_comp))
cpus = next((c for c in runtime.children if c.name == 'cpus'), None)
if cpus:
print('cpus:', ' '.join(c.name for c in cpus.children))
print(f'reordered {before} nodes')
if __name__ == '__main__':
main()
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Record, from the host, exactly when the device disappears.
The UDP kernel log stops at the moment we lose either the device or the ECM
transport, and those look identical in the log. This samples three independent
signals with timestamps so they can be told apart afterwards:
usb every connected gadget's VID:PID, straight from sysfs
net ICMP reachability of the rescue/target address
hdmesg the host's own USB stack messages (disconnect/reset/enumeration)
A device-side reset shows the gadget vanishing from sysfs. Losing only the
network leaves the gadget present while ICMP stops.
Usage: host-monitor.py <out.txt> [seconds] [target-ip]
"""
import os
import re
import subprocess
import sys
import time
SYS = "/sys/bus/usb/devices"
def gadgets():
"""Current set of VID:PID strings, read directly from sysfs."""
out = set()
try:
names = os.listdir(SYS)
except OSError:
return out
for name in names:
if ":" in name: # interfaces, not devices
continue
try:
with open(f"{SYS}/{name}/idVendor") as f:
vid = f.read().strip()
with open(f"{SYS}/{name}/idProduct") as f:
pid = f.read().strip()
except OSError:
continue
if vid in ("1d6b", "2833", "18d1"): # root hubs, Oculus, Google
out.add(f"{name}={vid}:{pid}")
return out
def host_usb_log():
"""Host kernel USB lines, best effort; needs no root on most systems."""
try:
d = subprocess.run(["dmesg", "--notime", "-l", "info,warn,err"],
capture_output=True, text=True, timeout=5)
return [l for l in d.stdout.splitlines()
if re.search(r"usb|cdc_ether|ecm", l, re.I)][-3:]
except Exception:
return []
def main():
out_path = sys.argv[1]
limit = float(sys.argv[2]) if len(sys.argv) > 2 else 300.0
target = sys.argv[3] if len(sys.argv) > 3 else "10.42.0.2"
start = time.time()
prev_usb, prev_net, prev_log = None, None, None
with open(out_path, "w", buffering=1) as f:
f.write(f"# host-monitor start={time.strftime('%F %T')} target={target}\n")
while time.time() - start < limit:
t = time.time() - start
usb = gadgets()
net = subprocess.run(["ping", "-c1", "-W1", target],
capture_output=True).returncode == 0
if usb != prev_usb:
f.write(f"{t:8.3f} usb {' '.join(sorted(usb)) or '(none)'}\n")
prev_usb = usb
if net != prev_net:
f.write(f"{t:8.3f} net {'UP' if net else 'DOWN'}\n")
prev_net = net
log = host_usb_log()
if log != prev_log:
for line in log:
f.write(f"{t:8.3f} hdmesg {line}\n")
prev_log = log
time.sleep(0.25)
f.write(f"{time.time() - start:8.3f} monitor-end\n")
if __name__ == "__main__":
main()
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Receive the target's /dev/kmsg UDP stream (see qkx-switch) into a file.
Usage: klog-udp.py <output-file> [port]
Records are "prio,seq,usec,flags;text". A restarted reader on the target
replays the whole backlog, so records whose sequence number was already seen
are dropped. Every write is flushed: the target may die at any moment.
"""
import socket
import sys
out = open(sys.argv[1], "a", buffering=1, errors="replace")
port = int(sys.argv[2]) if len(sys.argv) > 2 else 5140
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", port))
seen = set()
partial = ""
while True:
data, _ = sock.recvfrom(65536)
partial += data.decode("utf-8", "replace")
*lines, partial = partial.split("\n")
for line in lines:
head = line.split(";", 1)[0].split(",")
if len(head) >= 2 and head[1].isdigit():
seq = int(head[1])
if seq in seen:
continue
seen.add(seq)
out.write(line + "\n")
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Pin a read-only copy of the modem_a partition (holds the ADSP/CDSP firmware
# images our pre-Android ADSP boot needs) the same way tools/os-install.sh pins
# system/vendor/odm/product: allocate a pinned file on userdata, write it with
# raw-block extent copies, verify, and record its extent map.
#
# The partition itself is only ever read (dd if=<partition>); nothing is
# written back to it. The target later mounts the pinned copy through
# dm-linear, exactly like the OS images, and never touches the raw partition.
#
# Usage: tools/os-install-modemfw.sh [out-dir]
set -euo pipefail
OUT=${1:-out/os}
HERE=$(cd "$(dirname "$0")/.." && pwd)
W=/data/local/tmp/qkx
NAME=modemfw
say() { printf '\n== %s\n' "$*"; }
sh_() { adb shell "su -c '$*'"; }
have() { adb exec-out "su -c 'test -f $1 && echo QKX_YES'" | grep -q QKX_YES; }
[ -s "$OUT/partitions" ] || { echo "missing $OUT/partitions; run tools/os-install.sh first"; exit 1; }
PART_DEV=$(awk '$1 == "modem_a" { print $2 }' "$OUT/partitions")
[ -n "$PART_DEV" ] || { echo "modem_a not found in $OUT/partitions"; exit 1; }
adb shell 'su -c id' | grep -q 'uid=0' || { echo 'rooted adb (su) required'; exit 1; }
mkdir -p "$OUT/maps"
if have "$W/img/$NAME.img"; then
echo "== $NAME: already installed, skipping (rm $W/img/$NAME.img to redo)"
else
say "$NAME: reading size of $PART_DEV"
BASE=${PART_DEV##*/}
BYTES=$(adb exec-out "su -c 'cat /sys/class/block/$BASE/size'" | tr -d '\r')
BYTES=$((BYTES * 512))
echo "$PART_DEV is $((BYTES / 1024 / 1024)) MiB"
say "$NAME: dumping $PART_DEV (read-only) to device scratch space"
sh_ "dd if=$PART_DEV of=$W/modemfw-src.img bs=1M"
say "$NAME: allocating pinned file"
BYTES=$(( (BYTES + 4095) / 4096 * 4096 ))
sh_ "sh $W/qkx-install.sh alloc $NAME $BYTES"
say "$NAME: writing raw blocks from the dump"
sh_ "sh $W/qkx-install.sh write $NAME $W/modemfw-src.img"
say "$NAME: verifying"
sh_ "sh $W/qkx-install.sh verify $NAME $W/modemfw-src.img"
sh_ "rm -f $W/modemfw-src.img"
fi
say 'collecting map'
adb exec-out "su -c 'sh $W/qkx-install.sh map $NAME'" | tr -d '\r' > "$OUT/maps/$NAME.map"
[ -s "$OUT/maps/$NAME.map" ] || { echo "empty map for $NAME"; exit 1; }
printf '%-11s %s extents\n' "$NAME" "$(wc -l < "$OUT/maps/$NAME.map")"
echo
echo "modemfw pinned and mapped; rerun tools/os-prep.sh to bundle the new map"
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# Install a custom Android OS into pinned files on the headset's userdata.
#
# Usage: tools/os-install.sh <ota-image-dir> [out-dir] [data-size-GiB]
#
# The OS images and a fresh /data image are stored as pinned f2fs files under
# /data/local/tmp/qkx, with their contents written directly to the partition
# blocks f2fs allocated to them. That is what makes them readable from the kexec
# target, which cannot decrypt userdata.
#
# This writes to the userdata partition, but only inside blocks allocated to
# these files. It never formats a partition and never touches the stock OS.
set -euo pipefail
OTA=${1:?usage: os-install.sh <ota-image-dir> [out-dir] [data-size-GiB]}
OUT=${2:-out/os}
DATA_GIB=${3:-16}
HERE=$(cd "$(dirname "$0")/.." && pwd)
W=/data/local/tmp/qkx
# system.img is the root filesystem; the rest mount inside it.
IMAGES=(system system_ext vendor odm product)
say() { printf '\n== %s\n' "$*"; }
sh_() { adb shell "su -c '$*'"; }
# su does not reliably propagate exit status, so test by echoing a marker.
have() { adb exec-out "su -c 'test -f $1 && echo QKX_YES'" | grep -q QKX_YES; }
adb shell 'su -c id' | grep -q 'uid=0' || { echo 'rooted adb (su) required'; exit 1; }
for f in "${IMAGES[@]}"; do
[ -f "$OTA/$f.img" ] || { echo "missing $OTA/$f.img"; exit 1; }
done
mkdir -p "$OUT/maps"
say 'building device helpers'
make -C "$HERE/tools/device" >/dev/null
say 'pushing helpers'
adb push "$HERE/tools/device/qkx_fsmap" "$HERE/tools/device/qkx_rawcp" \
"$HERE/tools/device/qkx_dm" "$HERE/tools/device/qkx-install.sh" \
/data/local/tmp/ >/dev/null
sh_ "mkdir -p $W/bin $W/img $W/maps &&
cp /data/local/tmp/qkx_fsmap /data/local/tmp/qkx_rawcp /data/local/tmp/qkx_dm $W/bin/ &&
cp /data/local/tmp/qkx-install.sh $W/ &&
chmod 755 $W/bin/* $W/qkx-install.sh"
# Record which block device backs each partition name. The target has no vendor
# init to create by-name symlinks, so its initramfs needs this mapping.
say 'recording partition map'
adb exec-out "su -c 'sh $W/qkx-install.sh partitions'" | tr -d '\r' > "$OUT/partitions"
grep -E '^(userdata|persist|metadata) ' "$OUT/partitions" || {
echo 'partition map looks wrong:'; head -3 "$OUT/partitions"; exit 1; }
adb exec-out 'su -c "getprop ro.boot.slot_suffix"' | tr -d '\r' > "$OUT/slot_suffix"
echo "slot: $(cat "$OUT/slot_suffix")"
install_image() {
local name=$1 src=$2 bytes
bytes=$(stat -c %s "$src")
# f2fs allocates in 4 KiB blocks; round up so the whole image fits.
bytes=$(( (bytes + 4095) / 4096 * 4096 ))
say "$name: allocating $((bytes / 1024 / 1024)) MiB"
sh_ "sh $W/qkx-install.sh alloc $name $bytes"
say "$name: staging"
adb push "$src" "$W/stage.img" >/dev/null
say "$name: writing raw blocks"
sh_ "sh $W/qkx-install.sh write $name $W/stage.img"
say "$name: verifying"
sh_ "sh $W/qkx-install.sh verify $name $W/stage.img"
sh_ "rm -f $W/stage.img"
}
for name in "${IMAGES[@]}"; do
if have "$W/img/$name.img"; then
echo "== $name: already installed, skipping (rm $W/img/$name.img to redo)"
continue
fi
install_image "$name" "$OTA/$name.img"
done
if have "$W/img/data.img"; then
say "data: already installed, skipping (rm $W/img/data.img to redo)"
else
say "data: building empty ${DATA_GIB} GiB ext4"
DIMG=$OUT/data.img
rm -f "$DIMG" "$DIMG.gz"
truncate -s "${DATA_GIB}G" "$DIMG"
# Keep to features the 4.19 target kernel understands; newer e2fsprogs
# defaults (orphan_file, metadata_csum_seed) would make it unmountable.
mke2fs -q -t ext4 -b 4096 -I 256 -m 0 -L data -E nodiscard \
-O ^orphan_file,^metadata_csum_seed,^casefold,^fast_commit,^large_dir \
"$DIMG"
gzip -1 -c "$DIMG" > "$DIMG.gz"
ls -lh "$DIMG.gz"
say "data: allocating ${DATA_GIB} GiB pinned"
sh_ "sh $W/qkx-install.sh alloc data $((DATA_GIB * 1024 * 1024 * 1024))"
say 'data: staging compressed image'
adb push "$DIMG.gz" "$W/stage.img.gz" >/dev/null
say 'data: writing raw blocks (zero pass then image)'
sh_ "sh $W/qkx-install.sh writez data $W/stage.img.gz"
say 'data: verifying'
sh_ "sh $W/qkx-install.sh verify data $W/stage.img.gz"
sh_ "rm -f $W/stage.img.gz"
fi
say 'collecting maps'
for name in "${IMAGES[@]}" data; do
# Read the map back from the file itself, so a stale copy cannot slip in.
adb exec-out "su -c 'sh $W/qkx-install.sh map $name'" | tr -d '\r' > "$OUT/maps/$name.map"
[ -s "$OUT/maps/$name.map" ] || { echo "empty map for $name"; exit 1; }
printf '%-11s %s extents\n' "$name" "$(wc -l < "$OUT/maps/$name.map")"
done
say 'installed'
sh_ "sh $W/qkx-install.sh list"
echo
echo "maps and partition table saved in $OUT"
echo "next: tools/os-prep.sh $OUT <target-Image> out/payload"
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Build a kexec payload that assembles the custom OS instead of a bare shell.
# Usage: os-prep.sh <captured-dir> <target-Image> <os-dir> <out-dir>
#
# <os-dir> is what tools/os-install.sh produced (maps/ and partitions).
# The maps are baked into the initramfs, so re-run this after reinstalling an
# image: reallocating a pinned file hands out different blocks.
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
C=${1:?captured dir} IMG=${2:?target Image} OSDIR=${3:?os dir} OUT=${4:?out dir}
BB=${BUSYBOX:-$HERE/out/busybox}
[ -f "$BB" ] || { echo "no busybox at $BB; run: make initramfs"; exit 1; }
[ -f "$OSDIR/partitions" ] || { echo "no $OSDIR/partitions; run tools/os-install.sh"; exit 1; }
make -C "$HERE/tools/device" >/dev/null
mkdir -p "$(dirname "$OUT")"
RD=$OUT-initramfs.gz
python3 "$HERE/initramfs/build.py" --busybox "$BB" --output "$RD" \
--os-dir "$OSDIR" --qkx-dm "$HERE/tools/device/qkx_dm"
# A user build ignores androidboot.selinux; qkx_selinux_permissive (our kernel)
# keeps SELinux permissive so the debug shell survives init's policy load.
"$HERE/tools/prep.sh" "$C" "$IMG" "$RD" "$OUT" androidboot.selinux=permissive qkx_selinux_permissive=1 \
hung_task_panic=1 hung_task_timeout_secs=30 softlockup_panic=1 audit=0 ${QKX_EXTRA_CMDLINE:-}
echo "payload with custom OS ready: $OUT"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Stream the target's kernel log to the host over telnet.
Everything is written to a host file as it arrives, so the log survives a
target hang or reset. Usage: qkx_follow.py <output-file> [grep-pattern]
"""
import os
import socket
import sys
import time
out = open(sys.argv[1], "ab", buffering=0)
pattern = sys.argv[2] if len(sys.argv) > 2 else ""
cmd = "qkx-klogwatch" + (f" -g '{pattern}'" if pattern else "") + "\n"
s = socket.create_connection((os.environ.get("QKX_HOST", "10.42.0.2"), 23), timeout=10)
time.sleep(0.5)
s.sendall(b"stty -echo 2>/dev/null\n" + cmd.encode())
s.settimeout(None)
while True:
try:
data = s.recv(65536)
except OSError:
break
if not data:
break
out.write(data)
out.write(b"\n[qkx_follow: connection closed]\n")
+85 -2
View File
@@ -13,19 +13,102 @@ 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
# Stock hyp-assigns secure ION pages to other VMs; they stay locked after
# kexec and any access from the next kernel hangs the CPU. Collect them as
# late as possible and hand them over as one no-map reserved-memory node.
DTB=$P/boot.dtb
SECMAP=$HERE/module/ion_secmap.ko
if [ -f "$SECMAP" ]; then
adb push "$SECMAP" $T/ion_secmap.ko >/dev/null
# Preserve Android's live hardware state by default. Stopping services
# drops hardware/firmware votes and prevents this target from starting.
if [ "${QKX_STOP_ANDROID:-0}" = 1 ]; then
adb shell "su -c 'stop; for s in cameraserver virtual_camera bootanim \
vendor.oculus.hardware.composer-service vendor.qti.hardware.display.composer \
vendor.qti.hardware.display.allocator; do stop \$s; done; sleep 8; \
echo 3 > /proc/sys/vm/drop_caches; sleep 2'"
fi
if [ "${GIVEBACK:-0}" = 1 ]; then
# Return the pages to HLOS with the stock unassign calls; only
# what the hypervisor refuses still needs reserving.
adb push "$HERE/module/qkx_giveback.ko" $T/qkx_giveback.ko >/dev/null
adb shell "su -c 'insmod $T/qkx_giveback.ko; dmesg'" | tac |
sed '/giveback: begin/q' > "$P/giveback.txt"
grep -o 'giveback: returned.*' "$P/giveback.txt" ||
{ echo "giveback did not run"; exit 1; }
grep -o 'giveback: kept 0x[0-9a-f]*-0x[0-9a-f]*' "$P/giveback.txt" |
sed 's/giveback: kept/ionsec:/' > "$P/ionsec.txt"
else
adb shell "su -c 'insmod $T/ion_secmap.ko; dmesg'" | tac | sed '/ionsec: begin/q' |
grep -o 'ionsec: 0x[0-9a-f]*-0x[0-9a-f]*' > "$P/ionsec.txt"
[ -s "$P/ionsec.txt" ] || { echo "ion_secmap returned no ranges"; exit 1; }
fi
fi
if [ -s "$P/ionsec.txt" ] && [ -f "$SECMAP" ]; then
DTB=$(mktemp --suffix=.dtb)
cp "$P/boot.dtb" "$DTB"
# Early memblock can't grow its region array, so every no-map range
# costs a split; merge into a few 2 MiB-aligned blocks.
REG=$(python3 - "$P/ionsec.txt" <<'EOF'
import re, sys
A = 2 << 20
m = []
for a, b in sorted(tuple(int(x, 16) for x in re.findall(r'0x[0-9a-f]+', l))
for l in open(sys.argv[1])):
a &= ~(A - 1)
b = (b + A - 1) & ~(A - 1)
if m and a <= m[-1][1]:
m[-1][1] = max(m[-1][1], b)
else:
m.append([a, b])
print(' '.join('0x%x 0x%x 0x%x 0x%x' % (a >> 32, a & 0xffffffff,
(b - a) >> 32, (b - a) & 0xffffffff) for a, b in m))
EOF
)
fdtput -c "$DTB" /reserved-memory/qkx-ionsec &&
fdtput -t x "$DTB" /reserved-memory/qkx-ionsec reg $REG &&
fdtput -t s "$DTB" /reserved-memory/qkx-ionsec no-map "" ||
{ echo "failed to add ionsec reservation"; exit 1; }
echo "reserved $(wc -l < "$P/ionsec.txt") secure ranges as $(($(echo $REG | wc -w) / 4)) blocks"
fi
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 push "$DTB" $T/qkx-boot.dtb >/dev/null
adb shell 'su -c "sync; sync"'
for pair in "$MOD:qkx.ko" "$P/Image:qkx-Image" "$P/initramfs:qkx-initramfs" "$P/boot.dtb:qkx-boot.dtb"; do
for pair in "$MOD:qkx.ko" "$P/Image:qkx-Image" "$P/initramfs:qkx-initramfs" "$DTB:qkx-boot.dtb"; do
l=${pair%%:*}; r=${pair##*:}
[ "$(md5sum < "$l" | cut -d' ' -f1)" = "$(adb shell "su -c 'md5sum $T/$r'" | awk '{print $1}')" ] ||
{ echo "hash mismatch: $r"; exit 1; }
done
# The PMIC latches why the SoC last went down. Read it here, before this run
# overwrites it, so a failed attempt can be attributed afterwards.
adb shell "su -c 'dmesg | grep -iE \"Power-on reason|Power-off reason\"'" 2>/dev/null |
sed 's/^\[[^]]*\] *//' | tr -d '\r' > "$P/prev-reset-reason.txt"
[ -s "$P/prev-reset-reason.txt" ] &&
echo "previous shutdown: $(grep -m1 -i 'Power-off reason' "$P/prev-reset-reason.txt" | sed 's/.*Power-off reason: //')"
# QSEE listeners are registered in TrustZone against stock-kernel buffers.
# Freezing their owners preserves those stale registrations across kexec, and
# the target then hangs in __qseecom_scm_call2_locked. Stop only TEE clients;
# do not use Android's broad `stop`, which drops required hardware votes.
if [ "${QKX_STOP_QSEE:-0}" = 1 ]; then
adb shell "su -c 'for s in vendor.oculus.hardware.attestation-service \
vendor.oculus.devicecert-hal-1-0 gatekeeperd gatekeeper-1-0 \
vendor.keymaster-4-1 vendor.spdaemon vendor.qseecomd; do \
setprop ctl.stop \$s; done; sleep 2; \
echo qsee_services_stopped; \
ps -A | grep -E \"qseecomd|spdaemon|keymaster|gatekeeper|attestation|devicecert\" || true'" | tr -d '\r'
fi
# 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
# Assignments made after the ion_secmap snapshot would be missing from the
# reservation; hypwatch (if loaded) shows whether that window saw any.
adb shell "su -c 'cat /proc/qkx_hypwatch 2>/dev/null'" > "$P/hypwatch.txt" &&
[ -s "$P/hypwatch.txt" ] && echo "hypwatch before jump: $(head -1 "$P/hypwatch.txt")"
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
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Ask stock Android why the SoC went down last time.
#
# The PMIC latches a power-off reason across the reset, so after a failed run
# this distinguishes causes that are otherwise indistinguishable in our logs:
#
# PS_HOLD the SoC asked for the reset (kernel restart, watchdog bite, TZ)
# PMIC_WD the PMIC's own watchdog expired
# UVLO the supply collapsed (battery/charging)
# TFT/OTST3 thermal
# GP_FAULT/MBG_FAULT/OVLO PMIC fault
#
# Run it on stock, after the device has come back up. Read-only.
set -uo pipefail
T=${1:-}
run() { adb shell "su -c '$1'" 2>/dev/null | tr -d '\r'; }
echo "=== PMIC power-on / power-off reasons (latched across the reset) ==="
run 'dmesg | grep -iE "Power-on reason|Power-off reason"' | sed 's/^\[[^]]*\] *//'
echo
echo "=== Android's view of the boot reason ==="
run 'getprop ro.boot.bootreason; getprop sys.boot.reason; getprop ro.boot.alarmboot'
echo
echo "=== PMIC sysfs (same data, unparsed) ==="
run 'for f in /sys/devices/platform/soc/*/*/*/*power-on*/power_o*_reason; do
[ -r "$f" ] && echo "$f: $(cat $f)"; done'
echo
echo "=== Did anything survive in pstore? (warm reset only) ==="
run 'ls -l /sys/fs/pstore/ 2>&1 | head'
echo
echo "=== Battery / charger state now ==="
run 'dumpsys battery | grep -iE "level|powered|status|voltage|temperature"'
[ -n "$T" ] && { echo; echo "(saved to $T)"; }