Skip to content

feat(armv8/iommu): add SMMUv3 stage-2 driver - #382

Open
rabara wants to merge 1 commit into
bao-project:mainfrom
rabara:add-smmuv3-support
Open

feat(armv8/iommu): add SMMUv3 stage-2 driver#382
rabara wants to merge 1 commit into
bao-project:mainfrom
rabara:add-smmuv3-support

Conversation

@rabara

@rabara rabara commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Add a minimal ARM SMMUv3 (IHI 0070) driver for Armv8-A, providing stage-2-only DMA translation. The driver takes over the non-secure SMMU left enabled by firmware: it installs a linear stream table (all streams BYPASS by default so masters keep working with their incoming attributes), a command queue and an event queue, and promotes individual streams to stage-2 as VMs bind them. Faults are drained and logged via the event queue (wired SPI plus a cheap poll hook on the GIC IRQ path).

Driver selection is done at build time via SMMU_VERSION (default 2): arch/smmu.h resolves to smmuv2.h or smmuv3.h, and objects.mk builds the matching driver. Platforms opt into v3 with SMMU_VERSION:=3 in their platform.mk.

Verified working on Agilex5 SoCFPGA platform.

@joaopeixoto13 joaopeixoto13 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this. An SMMUv3 driver has been sitting in our "someday" pile for some time, so it's great to see a clean, focused driver actually land!

Did a close pass and two things I'd want sorted before moving on, and a couple of design questions that aren't blockers.

Functional issues

  1. Init-order race on the poll hook. smmu.evtq_prod gets published near the top of smmuv3_init(), right after the mem_alloc_map_dev(), but smmu.evtq isn't allocated and EVENTQ_BASE isn't programmed until much further down. smmuv3_poll_events() only checks evtq_prod != NULL and fires from gic_handle() on every core for every IRQ, so any IPI landing on a secondary CPU while the boot CPU is still mid-init will latch a stale PROD/CONS delta and walk smmu.evtq while it's still NULL. Cleanest fix is a ready flag flipped last, once SMMUEN is acked, with the poll hook gated on that instead of on the pointer. Left specifics inline.

  2. No SMMU TLBI on stage-2 remap. You issue TLBI_S12_VMALL at bind time, but nothing hangs off the stage-2 invalidation path (core/inc/tlb.h, the // TODO: inval iommu tlbs sites), so TLBI VMALLS12E1IS on the CPU side never reaches the SMMU's TLBs. Fine as long as every device mapping is static, which is almost certainly why it comes up clean, but the moment a bound VM's stage-2 gets touched the SMMU keeps hitting stale walk-cache/TLB entries and translating against the old tables. This PR is what finally makes those TODOs actionable, so it's the natural spot to hook SMMU invalidation into that path, or at least to spell out the static-mapping assumption in the docs.

Design questions (not blockers)

  • BYPASS-by-default vs abort-by-default. Any streamID without a group stays parked on a BYPASS STE, i.e. free-for-all DMA into any VM's PA space. Reasonable for a transparent firmware handoff, but it means the SMMU is acting as an opt-in translator rather than an isolation boundary: a master nobody enumerated sails through instead of getting terminated. Once a platform's streamID map is fully known you'd usually want the unbound default to be abort (invalid STE, faults land in the event queue). Any appetite for a build/per-platform knob to flip the default?

  • Single instance. One static struct smmuv3_priv smmu plus a single platform.arch.smmu bakes in one SMMU per SoC. Multi-SMMU parts are increasingly common, and a streamID is only unique within a given instance's stream table, so the config eventually needs a per-group "which SMMU" selector. Nothing to do here, just worth not boxing the interface in now.

Rest is smaller stuff and a few latent-but-not-yet-reachable bits, all inline. Thanks again for putting this up.

vaddr_t regs = mem_alloc_map_dev(&cpu()->as, SEC_HYP_GLOBAL, INVALID_VA,
platform.arch.smmu.base, NUM_PAGES(SMMUV3_PAGE1_EVENTQ_CONS + 4));
smmu.hw = (volatile struct smmuv3_hw*)regs;
smmu.evtq_prod = (volatile uint32_t*)(regs + SMMUV3_PAGE1_EVENTQ_PROD);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evtq_prod/evtq_cons are published early in smmuv3_init(), but smmu.evtq is allocated later and EVENTQ_BASE programmed later still. smmuv3_poll_events() (called from gic_handle() on every CPU, every IRQ) gates only on evtq_prod == NULL, so an IPI taken by another core during init can read a stale PROD/CONS pair, and dereference smmu.evtq while it's still NULL.

Suggest a dedicated ready flag published last. Sketch (spans three spots, so not a one-click suggestion):

/* in struct smmuv3_priv, alongside `coherent`: */
bool coherent;
bool ready;          /* set once init completes; gates the poll hook */
spinlock_t lock;

/* in smmuv3_poll_events(), replace the NULL gate: */
if (!smmu.ready) {
    return;
}

/* at the very end of smmuv3_init(), after SMMUEN is acked: */
smmu.ready = true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agee - I will add ready flag along with release barrier before publishing ready

/* smmuv3.c — struct */
struct smmuv3_priv {
    ...
    bool coherent;
    volatile bool ready;   /* published last; gates the poll hook */
};

/* smmuv3_poll_events() */
void smmuv3_poll_events(void)
{
    if (!smmu.ready) {
        return;
    }
    /* Acquire: order the flag load before the queue-state loads below.
     * A control dependency alone doesn't order load→load on ARMv8. */
    fence_ord_read();           /* DMB ISHLD */
    uint32_t prod = *smmu.evtq_prod;
}
/* end of smmuv3_init(), after the CR0ACK poll loop */

 /* Release: make every prior store (evtq pointer, zeroed queue memory,
 * PROD/CONS shadow init) visible to other observers before the flag.
 * Without this, another core can observe ready==true while smmu.evtq
 * is still NULL from its point of view.
 */
	fence_ord_write();          /* DMB ISHST — store→store ordering */
	smmu.ready = true;
	

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/arch/armv8/gic.c Outdated
#if (SMMU_VERSION == 3)
/* Surface any SMMU translation faults (a refused DMA usually coincides with
* a device error IRQ). Cheap no-op when the event queue is empty. */
smmuv3_poll_events();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs on every interrupt on every CPU and does two page-1 MMIO reads even in the common empty case: a measurable per-IRQ cost on interrupt-heavy workloads. It's a genuinely useful backstop where the wired eventq IRQ isn't reliably delivered, so I wouldn't drop it, but just consider scoping it. Cheapest option, restrict the poll to the master CPU (the wired IRQ handler already covers the general case):

#if (SMMU_VERSION == 3)
    if (cpu_is_master()) {
        smmuv3_poll_events();
    }
#endif

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I will update the PR with this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/arch/armv8/armv8-a/smmuv3.c Outdated
Comment on lines +27 to +29
/* Linear stream table cap: 2^8 = 256 entries (16 KiB). This comfortably covers
* typical single-level stream-ID spaces without needing a 2-level table. */
#define SMMUV3_ST_LOG2_CAP (8)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 256-entry cap assumes a small, dense stream-ID space. Stream IDs are integrator-defined and can be sparse or start high (PCIe Requester IDs especially). On a platform whose IDs exceed 255, every smmuv3_write_ste_s2() fails its bounds check and VM init aborts, with the only fix being to edit this constant. The table is linear, so letting a platform override it costs only memory. Suggestion makes it overridable from platform.mk (e.g. -DSMMUV3_ST_LOG2_CAP=11):

Suggested change
/* Linear stream table cap: 2^8 = 256 entries (16 KiB). This comfortably covers
* typical single-level stream-ID spaces without needing a 2-level table. */
#define SMMUV3_ST_LOG2_CAP (8)
/* Linear stream table cap: 2^SMMUV3_ST_LOG2_CAP entries (default 2^8 = 256,
* 16 KiB). A platform whose stream-ID space is sparse or starts high can
* override this from its platform.mk (e.g. -DSMMUV3_ST_LOG2_CAP=11). */
#ifndef SMMUV3_ST_LOG2_CAP
#define SMMUV3_ST_LOG2_CAP (8)
#endif

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - I will update the PR code , so it can be overridable from platform.mk.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/arch/armv8/armv8-a/smmuv3.c Outdated
* wired event IRQ isn't delivered. No-op until the driver is initialised. */
void smmuv3_poll_events(void)
{
if (smmu.evtq_prod == NULL) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the init-race fix above. This is the gate that goes live too early. Once a ready flag exists, this line becomes if (!smmu.ready) return;. Flagging it here so the two spots are reviewed together.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/arch/armv8/gic.c Outdated
Comment on lines +168 to +169
bool gicd_get_pend(irqid_t int_id)
{
return (gicd->ISPENDR[GIC_INT_REG(int_id)] & GIC_INT_MASK(int_id)) != 0;
}
{ return (gicd->ISPENDR[GIC_INT_REG(int_id)] & GIC_INT_MASK(int_id)) != 0; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like unrelated reformatting (the one-lined bodies don't match the surrounding style and aren't part of the SMMUv3 change). Worth reverting to keep the diff focused:

Suggested change
bool gicd_get_pend(irqid_t int_id)
{
return (gicd->ISPENDR[GIC_INT_REG(int_id)] & GIC_INT_MASK(int_id)) != 0;
}
{ return (gicd->ISPENDR[GIC_INT_REG(int_id)] & GIC_INT_MASK(int_id)) != 0; }
bool gicd_get_pend(irqid_t int_id)
{
return (gicd->ISPENDR[GIC_INT_REG(int_id)] & GIC_INT_MASK(int_id)) != 0;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be due to my local clang-format check, will fix in PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/arch/armv8/gic.c
}
}

bool gicd_get_act(irqid_t int_id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as gicd_get_pend

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines +125 to +130
for (size_t i = 0; i < SMMUV3_POLL_LIMIT; i++) {
if ((smmu.hw->CMDQ_CONS & SMMUV3_Q_IDX_MASK(SMMUV3_CMDQ_LOG2SIZE)) ==
(smmu.cmdq_prod & SMMUV3_Q_IDX_MASK(SMMUV3_CMDQ_LOG2SIZE))) {
return;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync-completion check compares only the index bits and drops the wrap bit, so a completely full queue (PROD = CONS + size) is indistinguishable from empty. Not reachable today since every push sequence syncs within a few entries of a 128-deep queue, but latent if commands are ever batched before syncing. cmdq_prod already carries the full {wrap, index} (it's kept modulo 2×size), and hardware maintains the wrap bit in CMDQ_CONS, so comparing the full field is robust:

Suggested change
for (size_t i = 0; i < SMMUV3_POLL_LIMIT; i++) {
if ((smmu.hw->CMDQ_CONS & SMMUV3_Q_IDX_MASK(SMMUV3_CMDQ_LOG2SIZE)) ==
(smmu.cmdq_prod & SMMUV3_Q_IDX_MASK(SMMUV3_CMDQ_LOG2SIZE))) {
return;
}
}
uint32_t fullmask = (SMMUV3_Q_WRAP(SMMUV3_CMDQ_LOG2SIZE) << 1) - 1;
for (size_t i = 0; i < SMMUV3_POLL_LIMIT; i++) {
if ((smmu.hw->CMDQ_CONS & fullmask) == (smmu.cmdq_prod & fullmask)) {
return;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree - I will update the PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines +90 to +95
#define SMMUV3_IDR1_TABLES_PRESET (1U << 30)
#define SMMUV3_IDR1_QUEUES_PRESET (1U << 29)
#define SMMUV3_IDR1_CMDQS_OFF (21)
#define SMMUV3_IDR1_CMDQS_LEN (5)
#define SMMUV3_IDR1_EVTQS_OFF (16)
#define SMMUV3_IDR1_EVTQS_LEN (5)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These capability macros are defined but never used

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove these unused macros.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@rabara

rabara commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @joaopeixoto13

Thanks a lot for this. An SMMUv3 driver has been sitting in our "someday" pile for some time, so it's great to see a clean, focused driver actually land!

Did a close pass and two things I'd want sorted before moving on, and a couple of design questions that aren't blockers.

Functional issues

  1. Init-order race on the poll hook.

I agree with your suggestion and will update the code in this PR.
Additionally I am also thinking of adding a release barrier (fence_sync_write() / DSB) before publishing ready so the store can't be reordered ahead of the queue setup as seen by other observers — the flag alone isn't enough on a weakly-ordered core.

  1. No SMMU TLBI on stage-2 remap.

Agreed this is a real gap. It's not reachable in the current static-partitioning model.
My preference is to document the static-mapping assumption explicitly in this PR and track the full invalidation hook in future PR. What is your opinion?

I foresee the testing would be a major challenge as well.

Design questions (not blockers)

  • BYPASS-by-default vs abort-by-default.

The knob is cheap, low-risk, and strictly increases capability while keeping the safe default — worth adding.
How about per-platform knob (e.g. -DSMMUV3_DEFAULT_ABORT from platform.mk) that flips the unbound-stream default from BYPASS to an abort STE, while keeping BYPASS as the default so the transparent firmware handoff (and bring-up on platforms without a fully-enumerated streamID map) is unaffected. The cost is just which STE template the unbound entries get. I'll keep GBPA in BYPASS during the reconfiguration window regardless, so in-flight transactions during handoff don't fault.

  • Single instance.

Fair point. I'll leave single-instance as-is for now, but I agree the interface shouldn't be boxed in — a per-group "which SMMU" selector is the right eventual shape given stream IDs are only unique within an instance's stream table.
Shall we add a note in code about this as a known limitation so multi-SMMU support can be layered on without an interface break? Or you have some other thought?

@joaopeixoto13

Copy link
Copy Markdown
Member

Thanks for the review @joaopeixoto13

Thanks a lot for this. An SMMUv3 driver has been sitting in our "someday" pile for some time, so it's great to see a clean, focused driver actually land!
Did a close pass and two things I'd want sorted before moving on, and a couple of design questions that aren't blockers.

Functional issues

  1. Init-order race on the poll hook.

I agree with your suggestion and will update the code in this PR. Additionally I am also thinking of adding a release barrier (fence_sync_write() / DSB) before publishing ready so the store can't be reordered ahead of the queue setup as seen by other observers — the flag alone isn't enough on a weakly-ordered core.

Yeah, agreed. Just note it needs a barrier on both sides, not just the writer. The if (!ready) return; check only orders later stores, not later loads on ARM, so another core can still read smmu.evtq / *evtq_prod before it sees ready == true. Easiest fix is to make ready a load-acquire (LDAR) on the reader, or put a DMB LD right after the gate. Then it's fully closed.

  1. No SMMU TLBI on stage-2 remap.

Agreed this is a real gap. It's not reachable in the current static-partitioning model. My preference is to document the static-mapping assumption explicitly in this PR and track the full invalidation hook in future PR. What is your opinion?

I foresee the testing would be a major challenge as well.

Fine to split it. Nothing hits that path in the static model today, so documenting the assumption now and doing the real hook later works for me. Two small asks: drop a note at the two // TODO: inval iommu tlbs lines in core/inc/tlb.h (not just the driver) so anyone adding dynamic remap later runs into it, and open an issue to track the actual hook.

And yeah, testing is the hard bit. You can't really trigger the stale-TLB case without remapping an already-bound VM's stage-2, which the static model never does, so the test only makes sense once the invalidation code exists. Fine to defer.

Design questions (not blockers)

  • BYPASS-by-default vs abort-by-default.

The knob is cheap, low-risk, and strictly increases capability while keeping the safe default — worth adding. How about per-platform knob (e.g. -DSMMUV3_DEFAULT_ABORT from platform.mk) that flips the unbound-stream default from BYPASS to an abort STE, while keeping BYPASS as the default so the transparent firmware handoff (and bring-up on platforms without a fully-enumerated streamID map) is unaffected. The cost is just which STE template the unbound entries get. I'll keep GBPA in BYPASS during the reconfiguration window regardless, so in-flight transactions during handoff don't fault.

Sounds good, that's exactly what I'd want: -DSMMUV3_DEFAULT_ABORT per platform, BYPASS as the default, GBPA staying in BYPASS during the reconfig window. All good.

  • Single instance.

Fair point. I'll leave single-instance as-is for now, but I agree the interface shouldn't be boxed in — a per-group "which SMMU" selector is the right eventual shape given stream IDs are only unique within an instance's stream table. Shall we add a note in code about this as a known limitation so multi-SMMU support can be layered on without an interface break? Or you have some other thought?

Yeah, a code note is enough. I'd put it in two spots: at static struct smmuv3_priv smmu (deliberately single-instance) and at the smmu_group struct (where a per-group instance selector would go later). That way someone can add multi-SMMU support down the line without breaking the interface. No need to do any of it now.

Add a minimal ARM SMMUv3 (IHI 0070) driver for Armv8-A, providing
stage-2-only DMA translation. The driver takes over the non-secure
SMMU left enabled by firmware: it installs a linear stream table
(all streams BYPASS by default so masters keep working with their
incoming attributes), a command queue and an event queue, and
promotes individual streams to stage-2 as VMs bind them. Faults are
drained and logged via the event queue (wired SPI plus a cheap poll
hook on the GIC IRQ path).

Driver selection is done at build time via SMMU_VERSION (default 2):
arch/smmu.h resolves to smmuv2.h or smmuv3.h, and objects.mk builds
the matching driver. Platforms opt into v3 with SMMU_VERSION:=3 in
their platform.mk; all existing platforms keep SMMUv2 unchanged.

Signed-off-by: Niravkumar L Rabara <niravkumarlaxmidas.rabara@altera.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants