简体中文 / [English]


Running Linux/Windows on ARM via Gunyah Virtualization on Qualcomm Android Phones (Debian/Windows VM on 8 Elite)

 

This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.

In 2024, I spent some time exploring how to run Linux and even Windows on Android using KVM virtualization. I compiled resources, did some kernel porting work, and (probably for the first time ever) figured out how to run Windows on ARM on MediaTek SoCs like the Dimensity 1100/1200 that allow access to EL2:

Running Linux/Windows on ARM via KVM on Android
Exploring Productivity on Android – Running Windows/Linux, Video Output, Kernel Compilation, KVM Virtualization, and More

(I recommend reading the above articles first — I cover a lot of background on virtualization there, which I won’t repeat here.)

Now fast forward to 2026. With Google pushing hard, Android 15 devices now require native virtualization support out of the box. Android 16 even ships with a built-in Terminal app capable of running Debian VMs. (Though it also brings the nasty trusted computing — but hey, evil intentions sometimes lead to half-decent outcomes.)

On my OnePlus 13, the Snapdragon 8 Elite CPU and GPU already outclass my laptop (i3-11300H) by a wide margin. It’s a shame to waste all that power just launching QQ, WeChat, and Alipay — China’s infamous trio of bloated apps. Why not put it to better use by running a real VM?

Current State of Virtualization

Let’s take a look at where virtualization stands today. Beyond Google’s Tensor chips with pKVM support, both Qualcomm and MediaTek have now answered the call been forced to add virtualization support in their newer chips.

Qualcomm

Unlike Tensor’s pKVM, Qualcomm introduced Gunyah support starting from the Snapdragon 8 Gen 2. (What the hell is “Gunyah” anyway?)

Gunyah is Qualcomm’s in-house open-source hypervisor, running at EL2 and occupying EL2. It provides necessary interfaces for the EL1 kernel to run trusted VMs. On Linux (Android), it shows up as /dev/gunyah — not the more common and widely supported /dev/kvm.

(As we know from earlier discussions, if EL2 were directly handed to the kernel, the kernel could provide KVM support. So this extra layer clearly exists because the system doesn’t trust the kernel — likely to enable anti-user features like DRM or closed-source on-device LLMs. Thanks, Google, f**k you.)

While Gunyah has technically been supported since 8 Gen 2, early implementations had issues — such as forcing the use of proprietary firmware, requiring workarounds. These were only resolved on 8 Elite and newer devices.

MediaTek

MediaTek, on the other hand, introduced GenieZone starting with the Dimensity 9000 series — similar in concept to Qualcomm’s approach. It’s their own hypervisor running at EL2, exposed as /dev/gzvm.

According to online sources, since GenieZone supports regular (non-memory-protected) VMs, Dimensity 9000 series devices can directly launch Debian VMs via Android 16’s Terminal app. After unlocking the bootloader and rooting, one might even run custom VMs via crosvm. However, I don’t have a device to test this, so I won’t cover GenieZone usage here.

Other Vendors

Samsung’s Exynos 2500 appears to support running Debian via the Terminal app, though the underlying technology remains unclear.

Google’s full lineup of Tensor chips, as mentioned earlier, supports native KVM and pKVM — allowing direct QEMU usage.

As for mysterious in-house chips from other Chinese vendors, due to lack of public documentation and no access to actual devices, their virtualization support remains unknown.

Exploring Gunyah for Running VMs

As of this writing, all released Qualcomm devices (up to the latest 8 Elite Gen 5) only support protected VMs. Even if you install Android 16, the built-in Terminal app won’t show up. If you somehow force it open, you’ll get an error: java.lang.UnsupportedOperationException: Non-protected VMs are not supported on this device.

Gunyah is open-source, but its implementation lives in the hyp partition, which is protected by XBL signature verification on production devices. Modifying it directly leads to a black brick or red-screen boot failure — even with unlocked bootloader (ABL). So unless you’re willing to risk a brick, you’re stuck with the vendor-provided Gunyah firmware.

Looks like we’ll have to dance in chains figure out how to run our own VMs under Gunyah’s memory-protected hypervisor.

Prerequisites

You’ll need a Snapdragon 8 Elite (or newer) device with unlocked bootloader and root access, running Android 15 or higher.

I used a OnePlus 13 running Android 16 (Evolution X 11), kernel version 6.6.126.

First, check if the necessary Gunyah device is available:

1
2
3
OP5D0DL1:/ $ su
OP5D0DL1:/ # ls /dev/gunyah
/dev/gunyah

Crosvm

QEMU doesn’t yet support calling these new Arm hypervisors. So we’re stuck with Google’s crosvm — an open-source VMM.

Android phones already include crosvm at /apex/com.android.virt/bin/crosvm, so this should be straightforward.

Based on Qualcomm’s documentation and crosvm’s help text, it doesn’t look complicated — we should be able to boot Linux using the Linux boot protocol.

Download debian-12-nocloud-arm64.raw from Debian’s cdimage site, and extract the initrd and vmlinuz (Linux kernel) files on a regular Linux machine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test@epyc:~$ fdisk -l debian-12-nocloud-arm64.raw
(...)
Device Start End Sectors Size Type
debian-12-nocloud-arm64.raw1 262144 6289407 6027264 2.9G Linux root (ARM-64)
debian-12-nocloud-arm64.raw15 2048 262143 260096 127M EFI System
test@epyc:~$ sudo losetup -Pf debian-12-nocloud-arm64.raw
test@epyc:~$ losetup -a
/dev/loop26: []: (/home/test/debian-12-nocloud-arm64.raw)
test@epyc:~$ sudo mount /dev/loop26p1 tmp
test@epyc:~$ cd tmp/boot && ls -l
total 61744
-rw-r--r-- 1 root root 291293 5月 27 05:29 config-6.1.0-49-arm64
drwxr-xr-x 2 root root 4096 6月 1 13:04 efi
drwxr-xr-x 5 root root 4096 6月 1 14:00 grub
-rw-r--r-- 1 root root 29958431 6月 1 14:00 initrd.img-6.1.0-49-arm64
-rw-r--r-- 1 root root 83 5月 27 05:29 System.map-6.1.0-49-arm64
-rw-r--r-- 1 root root 32954304 5月 27 05:29 vmlinuz-6.1.0-49-arm64

Grab initrd.img-6.1.0-49-arm64 and vmlinuz-6.1.0-49-arm64, then adb push them to /data/local/tmp on your phone. Also copy the debian-12-nocloud-arm64.raw file there.

Now, launch it with crosvm:

1
/apex/com.android.virt/bin/crosvm --log-level=debug run --hypervisor gunyah --disable-sandbox --cpus 2 --mem 1024 --initrd initrd.img-6.1.0-49-arm64 --protected-vm-without-firmware --block debian-12-nocloud-arm64.raw,lock=false vmlinuz-6.1.0-49-arm64

Logs scrolled happily for a second… then WTF, the phone went black.

Expand full log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
[2026-06-15T09:00:07.864025329+00:00 DEBUG crosvm::crosvm::sys::linux] creating hypervisor: Gunyah { device: None, qcom_trusted_vm_id: None, qcom_trusted_vm_pas_id: None }
[2026-06-15T09:00:07.866230329+00:00 INFO crosvm::crosvm::sys::linux::device_helpers] Trying to attach block device: debian-12-nocloud-arm64.raw
[2026-06-15T09:00:07.866881579+00:00 INFO disk] disk size 3221225472
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x000f0480]
[ 0.000000] Linux version 6.1.0-49-arm64 ([email protected]) (gcc-12 (Debian 12.2.0-14+deb12u1) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) #1 SMP Debian 6.1.174-1 (2026-05-26)
[ 0.000000] random: crng init done
[ 0.000000] Machine model: linux,dummy-virt
[ 0.000000] efi: UEFI not found.
[ 0.000000] NUMA: No NUMA configuration found
[ 0.000000] NUMA: Faking a node at [mem 0x0000000080000000-0x00000000bfffffff]
[ 0.000000] NUMA: NODE_DATA [mem 0xbbdef380-0xbbdf1fff]
[ 0.000000] Zone ranges:
[ 0.000000] DMA [mem 0x0000000080000000-0x00000000bfffffff]
[ 0.000000] DMA32 empty
[ 0.000000] Normal empty
[ 0.000000] Movable zone start for each node
[ 0.000000] Early memory node ranges
[ 0.000000] node 0: [mem 0x0000000080000000-0x00000000bfffffff]
[ 0.000000] Initmem setup node 0 [mem 0x0000000080000000-0x00000000bfffffff]
[ 0.000000] cma: Reserved 64 MiB at 0x00000000b6c00000
[ 0.000000] psci: probing for conduit method from DT.
[ 0.000000] psci: PSCIv1.1 detected in firmware.
[ 0.000000] psci: Using standard PSCI v0.2 function IDs
[ 0.000000] psci: MIGRATE_INFO_TYPE not supported.
[ 0.000000] psci: SMC Calling Convention v1.3
[ 0.000000] percpu: Embedded 31 pages/cpu s86760 r8192 d32024 u126976
[ 0.000000] Detected PIPT I-cache on CPU0
[ 0.000000] CPU features: detected: Address authentication (architected QARMA5 algorithm)
[ 0.000000] CPU features: detected: GIC system register CPU interface
[ 0.000000] CPU features: detected: Hardware dirty bit management
[ 0.000000] CPU features: detected: Spectre-v4
[ 0.000000] CPU features: detected: Spectre-BHB
[ 0.000000] alternatives: applying boot alternatives
[ 0.000000] Fallback order for Node 0: 0
[ 0.000000] Built 1 zonelists, mobility grouping on. Total pages: 258048
[ 0.000000] Policy zone: DMA
[ 0.000000] Kernel command line: panic=-1 console=ttyS0
[ 0.000000] Dentry cache hash table entries: 131072 (order: 8, 1048576 bytes, linear)
[ 0.000000] Inode-cache hash table entries: 65536 (order: 7, 524288 bytes, linear)
[ 0.000000] mem auto-init: stack:all(zero), heap alloc:on, heap free:off
[ 0.000000] Memory: 69064K/1048576K available (13248K kernel code, 2804K rwdata, 9504K rodata, 6464K init, 627K bss, 147972K reserved, 65536K cma-reserved)
[ 0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=2, Nodes=1
[ 0.000000] ftrace: allocating 44173 entries in 173 pages
[ 0.000000] ftrace: allocated 173 pages with 5 groups
[ 0.000000] trace event string verifier disabled
[ 0.000000] rcu: Hierarchical RCU implementation.
[ 0.000000] rcu: RCU restricting CPUs from NR_CPUS=256 to nr_cpu_ids=2.
[ 0.000000] Rude variant of Tasks RCU enabled.
[ 0.000000] Tracing variant of Tasks RCU enabled.
[ 0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 25 jiffies.
[ 0.000000] rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=2
[ 0.000000] NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0
[ 0.000000] GICv3: 988 SPIs implemented
[ 0.000000] GICv3: 1024 Extended SPIs implemented
[ 0.000000] Root IRQ handler: gic_handle_irq
[ 0.000000] GICv3: GICv3 features: 16 PPIs, DirectLPI
[ 0.000000] GICv3: CPU0: found redistributor 0 region 0:0x000000003ffb0000
[ 0.000000] rcu: srcu_init: Setting srcu_struct sizes based on contention.
[ 0.000000] arch_timer: cp15 timer(s) running at 19.20MHz (virt).
[ 0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x46d987e47, max_idle_ns: 440795202767 ns
[ 0.000000] sched_clock: 56 bits at 19MHz, resolution 52ns, wraps every 4398046511078ns
[ 0.000012] arm-pv: using stolen time PV
[ 0.000163] Console: colour dummy device 80x25
[ 0.000264] Calibrating delay loop (skipped), value calculated using timer frequency.. 38.40 BogoMIPS (lpj=76800)
[ 0.000265] pid_max: default: 32768 minimum: 301
[ 0.000346] LSM: Security Framework initializing
[ 0.000491] landlock: Up and running.
[ 0.000491] Yama: disabled by default; enable with sysctl kernel.yama.*
[ 0.000857] AppArmor: AppArmor initialized
[ 0.000858] TOMOYO Linux initialized
[ 0.000861] LSM support for eBPF active
[ 0.001006] Mount-cache hash table entries: 2048 (order: 2, 16384 bytes, linear)
[ 0.001103] Mountpoint-cache hash table entries: 2048 (order: 2, 16384 bytes, linear)
[ 0.004758] cblist_init_generic: Setting adjustable number of callback queues.
[ 0.004758] cblist_init_generic: Setting shift to 1 and lim to 1.
[ 0.004857] cblist_init_generic: Setting adjustable number of callback queues.
[ 0.004858] cblist_init_generic: Setting shift to 1 and lim to 1.
[ 0.005272] rcu: Hierarchical SRCU implementation.
[ 0.005272] rcu: Max phase no-delay instances is 1000.
[ 0.008720] EFI services will not be available.
[ 0.009280] smp: Bringing up secondary CPUs ...
[ 0.010535] Detected PIPT I-cache on CPU1
[ 0.010586] GICv3: CPU1: found redistributor 1 region 0:0x000000003ffd0000
[ 0.010712] CPU1: Booted secondary processor 0x0000000001 [0x000f0480]
[ 0.011185] smp: Brought up 1 node, 2 CPUs
[ 0.011187] SMP: Total of 2 processors activated.
[ 0.011188] CPU features: detected: Branch Target Identification
[ 0.011190] CPU features: detected: Instruction cache invalidation not required for I/D coherence
[ 0.011191] CPU features: detected: Data cache clean to the PoU not required for I/D coherence
[ 0.011191] CPU features: detected: Common not Private translations
[ 0.011191] CPU features: detected: CRC32 instructions
[ 0.011192] CPU features: detected: Data cache clean to Point of Deep Persistence
[ 0.011192] CPU features: detected: Data cache clean to Point of Persistence
[ 0.011192] CPU features: detected: E0PD
[ 0.011193] CPU features: detected: Enhanced Counter Virtualization
[ 0.011193] CPU features: detected: Enhanced Privileged Access Never
[ 0.011194] CPU features: detected: Generic authentication (architected QARMA5 algorithm)
[ 0.011194] CPU features: detected: RCpc load-acquire (LDAPR)
[ 0.011194] CPU features: detected: LSE atomic instructions
[ 0.011195] CPU features: detected: Privileged Access Never
[ 0.011195] CPU features: detected: Random Number Generator
[ 0.011195] CPU features: detected: Speculation barrier (SB)
[ 0.011196] CPU features: detected: TLB range maintenance instructions
[ 0.011196] CPU features: detected: Speculative Store Bypassing Safe (SSBS)
[ 0.011308] CPU: All CPU(s) started at EL1
[ 0.011341] alternatives: applying system-wide alternatives
[ 0.084859] node 0 deferred pages initialised in 68ms
[ 0.085557] devtmpfs: initialized
[ 0.087180] Registered cp15_barrier emulation handler
[ 0.087181] setend instruction emulation is not supported on this system
[ 0.087353] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[ 0.087502] futex hash table entries: 512 (order: 3, 32768 bytes, linear)
[ 0.087806] pinctrl core: initialized pinctrl subsystem
[ 0.088338] DMI not present or invalid.
[ 0.088837] NET: Registered PF_NETLINK/PF_ROUTE protocol family
[ 0.099750] DMA: preallocated 128 KiB GFP_KERNEL pool for atomic allocations
[ 0.099829] DMA: preallocated 128 KiB GFP_KERNEL|GFP_DMA pool for atomic allocations
[ 0.099900] DMA: preallocated 128 KiB GFP_KERNEL|GFP_DMA32 pool for atomic allocations
[ 0.099914] audit: initializing netlink subsys (disabled)
[ 0.100205] audit: type=2000 audit(0.092:1): state=initialized audit_enabled=0 res=1
[ 0.100635] thermal_sys: Registered thermal governor 'fair_share'
[ 0.100636] thermal_sys: Registered thermal governor 'bang_bang'
[ 0.100637] thermal_sys: Registered thermal governor 'step_wise'
[ 0.100638] thermal_sys: Registered thermal governor 'user_space'
[ 0.100638] thermal_sys: Registered thermal governor 'power_allocator'
[ 0.100645] cpuidle: using governor ladder
[ 0.100647] cpuidle: using governor menu
[ 0.100741] hw-breakpoint: found 6 breakpoint and 4 watchpoint registers.
[ 0.100855] ASID allocator initialised with 65536 entries
[ 0.101295] Serial: AMBA PL011 UART driver
[ 0.102526] KASLR enabled
[ 0.108613] HugeTLB: registered 1.00 GiB page size, pre-allocated 0 pages
[ 0.108615] HugeTLB: 0 KiB vmemmap can be freed for a 1.00 GiB page
[ 0.108616] HugeTLB: registered 32.0 MiB page size, pre-allocated 0 pages
[ 0.108617] HugeTLB: 0 KiB vmemmap can be freed for a 32.0 MiB page
[ 0.108618] HugeTLB: registered 2.00 MiB page size, pre-allocated 0 pages
[ 0.108619] HugeTLB: 0 KiB vmemmap can be freed for a 2.00 MiB page
[ 0.108619] HugeTLB: registered 64.0 KiB page size, pre-allocated 0 pages
[ 0.108620] HugeTLB: 0 KiB vmemmap can be freed for a 64.0 KiB page
[ 0.110973] ACPI: Interpreter disabled.
[ 0.111321] iommu: Default domain type: Translated
[ 0.111323] iommu: DMA domain TLB invalidation policy: strict mode
[ 0.111723] pps_core: LinuxPPS API ver. 1 registered
[ 0.111724] pps_core: Software ver. 5.3.6 - Copyright 2005-2007 Rodolfo Giometti <[email protected]>
[ 0.111728] PTP clock support registered
[ 0.111739] EDAC MC: Ver: 3.0.0
[ 0.112735] NetLabel: Initializing
[ 0.112737] NetLabel: domain hash size = 128
[ 0.112738] NetLabel: protocols = UNLABELED CIPSOv4 CALIPSO
[ 0.112748] NetLabel: unlabeled traffic allowed by default
[ 0.112857] vgaarb: loaded
[ 0.113088] clocksource: Switched to clocksource arch_sys_counter
[ 0.113924] VFS: Disk quotas dquot_6.6.0
[ 0.113956] VFS: Dquot-cache hash table entries: 512 (order 0, 4096 bytes)
[ 0.114501] AppArmor: AppArmor Filesystem Enabled
[ 0.114510] pnp: PnP ACPI: disabled
[ 0.117887] NET: Registered PF_INET protocol family
[ 0.118650] IP idents hash table entries: 16384 (order: 5, 131072 bytes, linear)
[ 0.119231] tcp_listen_portaddr_hash hash table entries: 512 (order: 1, 8192 bytes, linear)
[ 0.120913] Table-perturb hash table entries: 65536 (order: 6, 262144 bytes, linear)
[ 0.121264] TCP established hash table entries: 8192 (order: 4, 65536 bytes, linear)
[ 0.122650] TCP bind hash table entries: 8192 (order: 6, 262144 bytes, linear)
[ 0.122742] TCP: Hash tables configured (established 8192 bind 8192)
[ 0.123844] MPTCP token hash table entries: 1024 (order: 3, 24576 bytes, linear)
[ 0.123961] UDP hash table entries: 512 (order: 2, 16384 bytes, linear)
[ 0.124058] UDP-Lite hash table entries: 512 (order: 2, 16384 bytes, linear)
[ 0.124090] NET: Registered PF_UNIX/PF_LOCAL protocol family
[ 0.124118] NET: Registered PF_XDP protocol family
[ 0.124120] PCI: CLS 0 bytes, default 64
[ 0.124672] kvm [1]: HYP mode not available
[ 0.133305] Trying to unpack rootfs image as initramfs...
[ 0.157310] Initialise system trusted keyrings
[ 0.157337] Key type blacklist registered
[ 0.157752] workingset: timestamp_bits=42 max_order=18 bucket_order=0
[ 0.160068] zbud: loaded
[ 0.160383] integrity: Platform Keyring initialized
[ 0.160385] integrity: Machine keyring initialized
[ 0.160385] Key type asymmetric registered
[ 0.160386] Asymmetric key parser 'x509' registered
[ 0.935004] Freeing initrd memory: 29256K
[ 0.945282] alg: self-tests for CTR-KDF (hmac(sha256)) passed
[ 0.945446] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 247)
[ 0.945887] io scheduler mq-deadline registered
[ 0.946815] shpchp: Standard Hot Plug PCI Controller Driver version: 0.4
[ 0.946835] pci-host-generic 10000.pci: host bridge /pci ranges:
[ 0.946839] pci-host-generic 10000.pci: MEM 0x0002000000..0x0003ffffff -> 0x0002000000
[ 0.946840] pci-host-generic 10000.pci: MEM 0x00c0800000..0xffffffffff -> 0x00c0800000
[ 0.946842] PCI: OF: PROBE_ONLY enabled
[ 0.946847] pci-host-generic 10000.pci: ECAM at [mem 0x00010000-0x0100ffff] for [bus 00]
[ 0.946865] pci-host-generic 10000.pci: PCI host bridge to bus 0000:00
[ 0.946866] pci_bus 0000:00: root bus resource [bus 00]
[ 0.946866] pci_bus 0000:00: root bus resource [mem 0x02000000-0x03ffffff]
[ 0.946867] pci_bus 0000:00: root bus resource [mem 0xc0800000-0xffffffffff pref]
[ 0.946918] pci 0000:00:00.0: [8086:1237] type 00 class 0x060000
[ 0.947281] pci 0000:00:01.0: [1af4:1042] type 00 class 0x018000
[ 0.947354] pci 0000:00:01.0: BAR 0 [mem 0x02000000-0x02007fff]
[ 0.948106] pci 0000:00:02.0: [1af4:1045] type 00 class 0x088000
[ 0.948171] pci 0000:00:02.0: BAR 0 [mem 0x02008000-0x0200ffff]
[ 0.948965] pci 0000:00:03.0: [1b36:0011] type 00 class 0xffff00
[ 0.949027] pci 0000:00:03.0: BAR 0 [mem 0x02010000-0x0201000f]
[ 0.949472] pci 0000:00:00.0: Limiting direct PCI/PCI transfers
[ 0.950123] Serial: 8250/16550 driver, 4 ports, IRQ sharing enabled
[ 0.950442] printk: console [ttyS0] disabled
[ 0.950472] 3f8.U6_16550A: ttyS0 at MMIO 0x3f8 (irq = 13, base_baud = 115200) is a 16550A
[ 1.178708] printk: console [ttyS0] enabled
[ 1.179620] 2f8.U6_16550A: ttyS1 at MMIO 0x2f8 (irq = 14, base_baud = 115200) is a 16550A
[ 1.181239] 3e8.U6_16550A: ttyS2 at MMIO 0x3e8 (irq = 13, base_baud = 115200) is a 16550A
[ 1.182800] 2e8.U6_16550A: ttyS3 at MMIO 0x2e8 (irq = 14, base_baud = 115200) is a 16550A
[ 1.184316] Serial: AMBA driver
[ 1.184921] SuperH (H)SCI(F) driver initialized
[ 1.185819] msm_serial: driver initialized
[ 1.186945] mousedev: PS/2 mouse device common for all mice
[ 1.188346] ledtrig-cpu: registered to indicate activity on CPUs
[ 1.189643] SMCCC: SOC_ID: ARCH_SOC_ID not implemented, skipping ....
[ 1.221128] NET: Registered PF_INET6 protocol family
[ 1.225272] Segment Routing with IPv6
[ 1.226136] In-situ OAM (IOAM) with IPv6
[ 1.226979] mip6: Mobile IPv6
[ 1.227527] NET: Registered PF_PACKET protocol family
[ 1.228930] mpls_gso: MPLS GSO support
[ 1.230053] registered taskstats version 1
[ 1.230926] Loading compiled-in X.509 certificates
[ 1.240267] Loaded X.509 cert 'Build time autogenerated kernel key: a6fef1fd8620914aa952f48b963013957a2c4187'
[ 1.242988] zswap: loaded using pool lzo/zbud
[ 1.244541] Key type .fscrypt registered
[ 1.245364] Key type fscrypt-provisioning registered
[ 1.250202] Key type encrypted registered
[ 1.251037] AppArmor: AppArmor sha1 policy hashing enabled
[ 1.252448] ima: No TPM chip found, activating TPM-bypass!
[ 1.253723] ima: Allocated hash algorithm: sha256
[ 1.254737] ima: No architecture policies found
[ 1.255728] evm: Initialising EVM extended attributes:
[ 1.256863] evm: security.selinux
[ 1.257660] evm: security.SMACK64 (disabled)
[ 1.258476] evm: security.SMACK64EXEC (disabled)
[ 1.259443] evm: security.SMACK64TRANSMUTE (disabled)
[ 1.260732] evm: security.SMACK64MMAP (disabled)
[ 1.261787] evm: security.apparmor
[ 1.262585] evm: security.ima
[ 1.263272] evm: security.capability
[ 1.264191] evm: HMAC attrs: 0x1
[ 1.305240] clk: Disabling unused clocks
[ 1.308351] Freeing unused kernel memory: 6464K
[ 1.309723] Checked W+X mappings: passed, no W+X pages found
[ 1.310394] Run /init as init process
Loading, please wait...
Starting systemd-udevd version 252.39-1~deb12u2
[ 1.366901] virtio_blk virtio0: 2/0/0 default/read/poll queues
[ 1.370166] virtio_blk virtio0: [vda] 6291456 512-byte logical blocks (3.22 GB/3.00 GiB)

The log stopped at virtio_blk virtio0: [vda] 6291456 512-byte logical blocks (3.22 GB/3.00 GiB), and the phone entered 900E mode — only fixable via forced reboot.

Is there any way to debug in 900E mode on consumer devices? Probably requires some signed vendor tools I don’t have. Checking /sys/fs/pstore/console-ramoops yielded nothing.

The only useful clue was the crash seemed related to virtio. Given that protected VMs have memory protection — a major difference — I suspected the current virtio implementation wasn’t compatible with pVMs, causing the host to crash. But how to debug that? No idea.

I randomly tried different combinations of boot parameters and distros — no luck. Without block devices, it could boot into initramfs, but mounting a disk caused a black screen. So no way to boot Debian properly.

The Virtio Issue

This was my progress back in early April — then I hit a wall and paused. But serendipitously, a few days later, Bilibili recommended me a video: [First in the World] Running Windows Natively on Qualcomm Phones? BV1X7QvBEE18, uploaded on April 14.

It introduced GitHub@BigfootACA‘s DroidVM project — exactly what I was working on!

I dug into the code. At first glance, it used crosvm just like I did — no obvious difference. After comparing for a while, I noticed: it wasn’t using the system’s built-in crosvm, but a self-compiled one.

So I tried installing the DroidVM app and using its crosvm:

1
/data/data/cn.classfun.droidvm/usr/bin/crosvm --log-level=debug run --hypervisor gunyah --disable-sandbox --cpus 2 --mem 1024 --initrd initrd.img-6.1.0-49-arm64 --protected-vm-without-firmware --block debian-12-nocloud-arm64.raw,lock=false vmlinuz-6.1.0-49-arm64

Progress! crosvm didn’t crash, and I got actual error logs:

1
2
3
4
5
6
7
8
9
10
(ignoring duplicate logs...)
[ 1.381396] Freeing unused kernel memory: 6464K
[ 1.383930] Checked W+X mappings: passed, no W+X pages found
[ 1.384829] Run /init as init process
Loading, please wait...
Starting systemd-udevd version 252.39-1~deb12u2
[ 1.456299] virtio_blk virtio0: 2/0/0 default/read/poll queues
[ 1.458012] virtio_blk virtio0: [vda] 6291456 512-byte logical blocks (3.22 GB/3.00 GiB)
[2026-06-15T12:10:36.591592422+00:00 ERROR devices::virtio::queue::split_queue] get_avail_index: host access to lent memory region at 0x97663002 (purpose=GuestMemoryRegion) in protected VM
Timed out for waiting the udev queue being empty.

This version gave a clear error: get_avail_index: host access to lent memory region... in protected VM, instead of crashing the whole phone.

Finally, a human-readable error! And thanks to BigfootACA, the DroidVM author, who answered my question in an issue, I learned that protected VMs require special virtio handling. According to him, the issue is tied to the CONFIG_DMA_RESTRICT_POOL kernel option.

Using DroidVM’s prebuilt initramfs and vmlinuz, plus the correct root= cmdline, I successfully booted Debian!

1
/data/data/cn.classfun.droidvm/usr/bin/crosvm --log-level=debug run --hypervisor gunyah --disable-sandbox --cpus 2 --mem 1024 --initrd droidvm_initramfs --protected-vm-without-firmware --params "root=/dev/vda1" --block debian-12-nocloud-arm64.raw,lock=false droidvm_vmlinuz

First successful boot into shellFirst successful boot into shell

While writing this blog, I realized DroidVM’s crosvm is actually a fork: Droid-VM/crosvm. The very first commit, 39c1337, added the error message that prevents crashes.

Judging from the commit log, this fork includes many additional fixes and features. And the author clearly knows what he’s doing — unlike me, who was just stumbling around. He’s forked crosvm, EDK2, and even Windows guest drivers, doing serious development. Great, now I can just copy homework and finish this blog!

For testing, I confirmed that using the stock Android crosvm with DroidVM’s kernel also works. So the root cause is indeed related to kernel DMA handling:

1
/apex/com.android.virt/bin/crosvm --log-level=debug run --hypervisor gunyah --disable-sandbox --cpus 2 --mem 1024 --initrd droidvm_initramfs --protected-vm-without-firmware --params "root=/dev/vda1" --block debian-12-nocloud-arm64.raw,lock=false droidvm_vmlinuz

Running zcat /proc/config.gz | grep CONFIG_DMA_ shows CONFIG_DMA_RESTRICTED_POOL=y in DroidVM’s kernel, while the original image had # CONFIG_DMA_RESTRICTED_POOL is not set.

Memory OOM Issue

Great, now I can just copy homework and finish this blog!

Of course, I soon hit another problem: running Gunyah requires “lending” memory to the encrypted VM. But shortly after boot, Android gets filled with background garbage apps — memory becomes fragmented and tight. Unlike KVM, in pVM mode, allocating more than 1GB to crosvm often leads to vcpu hit unknown error: Out of memory (os error 12) during runtime.

Clearly, the DroidVM author hit this too. His solution? Droid-VM/gh-hugepage-reserve

It’s a Magisk module that loads a kernel module at boot, pre-allocating several 2MB hugepages. Using kprobe, it tracks processes that use Gunyah. When such processes call alloc_pages, it returns memory from the pool; on free_pages, it returns memory to the pool.

…Wait, is this even correct? Feels like brute force magic. Clearly, the author had no better options — but hey, it works.

You can control this via DroidVM’s UI or command line:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Install the gh-hugepage-reserve Magisk module
# Configure to pre-allocate 2048 * 2 = 4096MB RAM
OP5D0DL1:/ # echo 'pool_target=2048' > /data/adb/modules/gh-hugepage-reserve/settings.prop

# Reboot and check status
OP5D0DL1:/ # cat /sys/module/gh_hugepage_reserve/parameters/refill_stat
state=idle
pool_avail=2048
pool_total=2048
total_served=0
total_refilled=0
active_vms=0

# Use --hugepages and --prepare-lend-mthp with the patched crosvm
OP5D0DL1:/data/local/tmp # /data/data/cn.classfun.droidvm/usr/bin/crosvm --log-level=debug run --hypervisor gunyah --disable-sandbox --cpus 2 --mem 4096 --initrd droidvm_initramfs --protected-vm-without-firmware --params "root=/dev/vda1" --block debian-12-nocloud-arm64.raw,lock=false --hugepages --prepare-lend-mthp droidvm_vmlinuz
(...output omitted...)

# Check kernel module status
OP5D0DL1:/ # cat /sys/module/gh_hugepage_reserve/parameters/refill_stat
state=idle
pool_avail=32
pool_total=2048
total_served=4507
total_refilled=2491
active_vms=1

Now, crosvm runs without crashing due to memory pressure.

Testing

Now it’s time for standard VM tasks: set up TAP devices and NAT, connect to the internet, switch package sources, install SSH and other essentials, resize the disk, etc.

Thanks to the full Debian environment, all of this works exactly like on regular Debian — no extra hurdles.

SSH connectionSSH connection

Ran a sysbench single-core test: 11386 (vs. 270KP’s 4386). Not a fair cross-platform comparison, but at least it shows the 8 Elite’s power is being utilized.

I also tried compiling a Linux kernel to stress-test performance, but the phone got way too hot (SoC hitting 105°C), so I gave up.

Testing Windows

With DroidVM’s UEFI firmware, crosvm can boot Windows. Just replace the vmlinuz in boot args with the prebuilt edk2-gunyah.fd.

The PE image with built-in virtio drivers comes from Coolapk.

Ran a quick benchmark — Android system and apps were probably using some resources, and thermal throttling likely kicked in, but the scores still look decent:

CPU-Z arm64 native: single-core 592, multi-core 3093 — close to 8cx Gen 3CPU-Z arm64 native: single-core 592, multi-core 3093 — close to 8cx Gen 3

CPU-Z x64 emulation: single-core 388, multi-core 2041CPU-Z x64 emulation: single-core 388, multi-core 2041

Conclusion

Big thanks to the DroidVM project for making this blog possible — key steps and files came directly from it. While writing, I also forked relevant repos to help readers reproduce the setup.

I manually ran these commands to ensure technical accuracy, but if you just want to get a VM running, DroidVM is the easier choice. It’s actively developed, supports display output over Type-C, GPU passthrough, and many other features I didn’t cover. Definitely worth following. (Though I did run into a few UI bugs — but I trust the author will fix them eventually x)

This article is licensed under the CC BY-NC-SA 4.0 license.

Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/android-virtualization-on-snapdragon/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/