The Hypervisor Said No

Published 2026-09-19
The Hypervisor Said No
Series: Whose computer is it?Part 6 of 8

Notes from bringing up our own OS on a Samsung Galaxy Tab S6 (SM‑T865, Snapdragon 855 / SM8150). A continuation of the article about the "parasite" (the map of all the device's tenants is in the series introduction): there we found out that the Wi‑Fi firmware on this SoC DMAs into our memory by physical addresses. Here — why it turned out that way at the register level, who allowed it, who forbade doing otherwise, and what the way out looks like.


0. One write that did not land

The story is short. The WLAN firmware lives on the modem's Hexagon; its twelve copy engines walk into RAM through apps_smmu (0x1500_0000) as stream 0x640 (in the device tree — iommus = <&apps_smmu 0x640 0x1>: two ids, 0x640 and 0x641). For the DMA rings to work, the stream needs permission to pass. The simplest way on SMMUv2 is to write the type bypass into this stream's S2CR register: "do not translate, pass through as is". So we did.

The write did not land. The register read back with type fault. Not "stayed as before" — rewritten to fault. Someone between our instruction and the SMMU register looked at the value and replaced it with its opposite.

That "someone" is the hypervisor at EL2. On a Snapdragon it exists in every phone, and in the stock device tree apps_smmu carries the property qcom,skip-init: "do not initialise the global configuration, it is not yours".

1. SMMUv2 in five minutes

ARM SMMUv2 (MMU‑500, as Qualcomm fits it) is an MMU in front of DMA masters. A transaction arrives with a stream id — a number the NoC attributes to the source. Then, in order:

The path of one transaction and who writes what in it:

 copy engine          +----------------- apps_smmu (0x1500_0000) -----------------+
 (Hexagon, WLAN)      |                                                          |
 ---- sid 0x640 ----> |  SMR[i]  ---->  S2CR[i]  ---->  CB n: CBAR CBA2R SCTLR   | ----> stage 2 ----> DRAM
                      |  id/mask        type,cb        TTBR0 TCR MAIR  (stage 1) |       (EL2)         PA
                      +------|--------------|-------------------|----------------+         |
                             |              |                   |                          |
 who writes:            us (EL1)       us (EL1),          us (EL1):                  hypervisor
                        the write      but under EL2      SCTLR.M = 0 today,         (Samsung, EL2):
                        stays          veto:              M = 1 -- the plan          tables invisible
                                       BYPASS -> FAULT

 stream without SMR --> sCR0.USFCFG (global register: the hypervisor's) -----> bypass | fault

The middle belongs to us — SMR, S2CR (with a caveat) and the bank's contents. The global registers and stage 2 do not.

2. What "the hypervisor owns the global configuration" means

qcom,skip-init in the tree is an instruction to the driver: do not reset the SMMU, do not touch sCR0, do not consider yourself first. Someone configured the device before EL1 and keeps watching over it. On the tablet that is Samsung's hypervisor in hyp_mem (the no-map reserve from 0x8570_0000).

Three levels in practice:

Stock Android lives under the same conditions — with the same line in the tree and the same hypervisor.

The same picture one level up — not registers but tenants: who holds which layer of apps_smmu and where stream 0x640 reaches through it. Red is foreign, green is ours; a solid arrow is a mechanism observed live, a dashed one is "can, if it wants to" and what is marked as a plan. This is a fragment of the general device map from the introductory article (§ 2a), with the same node names.

Diagram: The Hypervisor Said No

3. The veto

The observed fact: a write of S2CR.TYPE = BYPASS comes back as FAULT. The hypervisor does not allow EL1 to declare a stream "without translation" directly. Why exactly — a policy of "every stream through a bank", or protection from a guest who accidentally opens everything to a master — we do not know. We know that on this SoC it cannot be written that way.

The driver distinguishes two failures. If there was a candidate bank and the check after the write shows something other than what was written — SmmuError::Vetoed: the writes did not land. If no free banks were found at all — NoRoom.

4. The workaround: pass‑through in the guise of translation

What stock Android does for a master without stage‑1 tables (icnss with the property qcom,smmu-s1-bypass), and what our map_passthrough_group repeats:

  1. Find an SMR already naming sid, otherwise a free one; write 0x640 with mask 0x1.
  2. Find a context bank that no valid S2CR of type translate points to — occupied neither by the hypervisor nor by another master. No more than eight candidates are tried.
  3. In CBAR — "stage 1 with stage 2 bypassed" (the hypervisor will substitute its own stage 2 himself), attributes non‑shareable, write‑back; in CBA2R — the 64‑bit format.
  4. In SCTLR — zero. M = 0: translation off, attributes as they come. There is a bank, there are no tables, the address passes as it came.
  5. In S2CR — type translate with this bank's number.
  6. Read disposition(sid) and make sure the stream is now Translate { cb: ours, sctlr.M = 0 }. If not — the next candidate; if they run out — Vetoed.

The hypervisor lets this through: from his point of view the stream goes through a bank, by the rules. From the master's point of view the address after stage 1 is unchanged. It is bypass dressed up as translation.

All our DMA masters go the same way: USB (0x140), UFS (0x300), QUP/GPI on the I²C buses. Some banks are inherited from the bootloader: UFS was read live as Translate { smr 2, cb 1, sctlr 0xe0, fsr 0x400 } — a bank with M = 0, already pass‑through; 0x400 is bit 10 FORMAT, a format indicator, not a fault: the bank recorded nothing. Such a bank map_passthrough_group leaves in place and only clears FSR. The display (0x800) on this boot is also pass‑through with SCTLR.M = 0.

In the platform's resource table this is one row — smmu:wlan, "wlan firmware dma: apps smmu stream 0x640/1", the switch Switch::Stream. "On" for it is Bypass or Translate with M = 0; "off" is a no‑op, what is on stays on. wifi.power votes for it before modem start: the firmware's first access to memory through an unconfigured stream under the hypervisor is not an error but a silent reset of the whole SoC (on CAL_REPORT).

5. What the hypervisor's stage 2 protects — and what it does not

What it protects from. From the guest and its masters — the hypervisor's memory, the secure world, other processors' regions, everything that is not in his tables for us. What exactly is there, we do not know and cannot check.

What it does not protect from, by construction. Stage 2 maps the guest's memory. Our kernel, the heap, the store with network passwords and tokens, the DMA rings in LENT_WLAN — to the hypervisor these are one entity, "the guest's RAM"; he does not distinguish our code from our DMA buffer. If he did, our own pass‑through would not work. So everything our processor can write to, a master behind a bank with M = 0 can write to as well.

Hence the formulation of the threat model: on our part of the system, stream 0x640 today addresses physical memory. The address in the rings' SR_BASE_LO/HI and DR_BASE_LO/HI is the physical address of LENT_WLAN (0x84e0_0000, 4 MiB of non‑cacheable RAM below hyp_mem). Honest firmware writes there. Compromised firmware will put any other address in a descriptor — and at the apps_smmu level nothing will stop it. The remaining fences — Samsung's stage 2 and Qualcomm's XPU — are not ours; we do not see their contents and cannot count them as our boundary.

6. A lens, not a driver

Since the SMMU is not ours, Smmu deliberately does almost nothing. It is an instrument of observation plus one operation:

No "reset the SMMU", "enable the port", "set USFCFG". Global state (sCR0, IDR0/1, sGFSR) is read, not written. Every master, when brought up, reports its disposition before and after — in the bring‑up log, in the hw.ufs and hw/panel facts. That way "who owns what" is not lost between runs.

One remark about USFCFG: a stream without an SMR, with the bit set, is dropped, and on this SoC such a fault is for the processor not an event but a hang. So "just don't touch the SMMU" for a new master is not an option; each one needs an explicit SMR and a bank.

A digression: powering the translation units

The SMMU is not a single block. The translation units (TBUs) sit on the NoC next to the masters, and they have their own power domains in the GCC: tbu1_gdsc and tbu2_gdsc (hlos1_vote_aggre_noc_mmu_tbu*_gdsc; TBU2 — streams 0x400..0x800 on the aggregate NoC, TBU1 — below). The bootloader leaves them off. The symptom is not a fault: the master's first access to memory waits forever, and the processor that wrote its doorbell waits with it. In the resource table both domains are vital — the USB console rests on them, and the HAL refuses to switch them off from above. "Does the SMMU work" on this chip is a question not only of configuration but of power.

7. Next: a real stage 1 for 0x640

This is a plan. Not done live.

The idea is direct: the same bank, the same S2CR of type translate — but SCTLR.M = 1 and filled TTBR0/TCR/MAIR. The tables map only the LENT_WLAN window, identity inside (IOVA = PA) and nothing outside. The rings are programmed with the same addresses — for honest firmware nothing changes. DMA outside the window is a bank fault (CFCFG: terminate, not stall), not a write into memory. disposition(0x640) should show Translate { sctlr.M = 1 }, clear_faults in honest operation — zero.

In code: Smmu::map_translate(sid, mask, ttbr0, tcr, mair) next to map_passthrough_group; stage‑1 tables for the window; Switch::TranslatedStream instead of Switch::Stream for the smmu:wlan row. Then a prove‑run: wifi.power on to ready, a scan, joining a network.

The risks are named honestly. The hypervisor may veto a bank with M = 1 the same way he vetoes bypass. Addresses the firmware computes itself (memory per SERVICE_READY) may turn out to be outside what is mapped — then either a different IOVA layout, or an honest record that this path is closed on this board. Any deviation here, as with the whole bring‑up, is a silent reset.

Why we think this should work — a conclusion from mainline, not our observation. In mainline Linux, sm8150.dtsi gives wifi@18800000 iommus = <&apps_smmu 0x0640 0x1>. The arm-smmu-qcom.c driver keeps a list of clients that get an identity domain — mdss, adreno, *-mss-pil; qcom,wcn3990-wifi is not in it. So the DMA API for this device goes through dma-iommu, and the copy engines address IOVAs. The SM8150 HDK with the same WLAN.HL.3.2.0 firmware brings up Wi‑Fi in mainline. This says that stage 1 for 0x640 on this SoC with this firmware is possible and the reference board's hypervisor does not forbid it. Our tablet is a Samsung with its own hypervisor; whether the policy coincides, only a run will show.

And so as not to overestimate the result: stage 1 locks down the copy engines — the path through apps_smmu. The regions handed to the modem through memshare (MSA, the rmtfs buffer) it writes by its own path — Q6 through its own MMU and the XPU, bypassing apps_smmu. The cure there is different: minimal grants, and the fence is TrustZone's XPU.

8. Fact, plan, conclusion

The SMMU in your phone is not yours. You are given one layer of three and watched so that you do not switch it off entirely. While that layer is empty on our side, the only fence between the Wi‑Fi firmware and kernel memory is someone else's. Filling it is one bank, one page table and one run that will either confirm the conclusion from mainline or add a line to the list of what this hypervisor does not allow.


Sources. The lens driver: crates/qcom/src/smmu.rsDisposition, map_passthrough_group, SmmuError::{Vetoed, NoRoom}, clear_faults, FSR_FAULTS, register offsets. The platform: platforms/sdm855/src/soc.rsAPPS_SMMU, WLAN_SID, MDSS_SID, LENT_WLAN, TBU1_GDSC/TBU2_GDSC; platforms/sdm855/src/resources.rs — the smmu:wlan row; crates/qcom/src/resource.rs — the semantics of Switch::Stream; platforms/sdm855/src/storage.rs — the live read of the UFS bank; crates/qcom/src/i2c.rsUSFCFG as a hang. Documents: docs/wifi-sm8150.md (the fact table, § 8 "what cannot be seen from the AP"), docs/wifi-threat-model.md § 4, § 5 (level 2), § 6, § 7, docs/wifi-parasite-article.md § 2.2, § 8.

Part 6 of 8
  1. 1Whose Computer Is This? Yours, or…
  2. 2A Reset Without Witnesses
  3. 3The Megabyte That Killed the Modem
  4. 4The Parasite in the Tablet
  5. 523 Milliseconds to Lie
  6. 6The Hypervisor Said No
  7. 7The Password Is Ours, the Key Is the Modem's
  8. 8Trust the Wire?

Products in this article

0xd-ossecurityembeddedsmmu
Try it