From f41bb8fb228472a605c9334a1f5a9a3984463d94 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 10:19:02 +0200 Subject: [PATCH 01/10] soo: do not BUG() on a bogus snapshot size in write_snapshot() write_snapshot() dereferenced the user-space buffer directly to pick up the snapshot size, then fed that value to dma_alloc_coherent() and BUG_ON()'d the result. A caller passing an uninitialized or corrupted buffer therefore killed the agency: the size read back was garbage (1.7 GB in the observed case), the allocation warned in the page allocator and returned NULL, and the kernel died in an ioctl with the interrupts off. Read the size with copy_from_user(), reject a size that no snapshot could ever have, and return -EINVAL/-ENOMEM/-EFAULT instead of taking the machine down. The error is propagated through the WRITE_SNAPSHOT ioctl so that user space can report it. Observed with the EMISO engine restoring a snapshot whose archive held no capsule payload. --- .../files/soo-generic/0073-capsule.c.patch | 36 +++++++++++++++---- .../soo/files/soo-generic/0077-core.c.patch | 9 ++++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch index 7c85d0299..bd2bb3136 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/capsule.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/capsule.c 2025-08-30 15:37:09.704925348 +0200 -@@ -0,0 +1,627 @@ +@@ -0,0 +1,651 @@ +/* + * Copyright (C) 2017-2024 Daniel Rossier + * @@ -30,6 +30,7 @@ +#include +#include +#include ++#include +#include +#include +#include @@ -330,19 +331,42 @@ + uint32_t slotID; + S3C_state_t S3C_state; + -+ snapshot_size = *((uint32_t *) buffer); ++ /* The snapshot begins with its own size. The buffer belongs to user space, ++ * hence the copy_from_user() and the sanity check below: a corrupted or ++ * uninitialized buffer must be rejected here instead of being turned into ++ * an absurd allocation request. ++ */ ++ ++ if (copy_from_user(&snapshot_size, buffer, sizeof(snapshot_size))) ++ return -EFAULT; ++ ++ DBG("Retrieving a size of %u bytes.\n", snapshot_size); + -+ DBG("Retrieving a size of %d bytes.\n", snapshot_size); ++ if ((snapshot_size < sizeof(snapshot_size)) || ++ (snapshot_size > (totalram_pages() << PAGE_SHIFT))) { ++ printk("%s: Invalid snapshot size (%u bytes)\n", __func__, snapshot_size); ++ ++ return -EINVAL; ++ } + + /* + * Prepare a buffer to store the S3C and additional header information like the snapshot structure. + */ -+ me = dma_alloc_coherent(capsule_dev, snapshot_size, &dma_handle, GFP_KERNEL); -+ BUG_ON(!me); ++ me = dma_alloc_coherent(capsule_dev, snapshot_size, &dma_handle, GFP_KERNEL | __GFP_NOWARN); ++ if (!me) { ++ printk("%s: Failed to allocate %u bytes for the snapshot\n", __func__, snapshot_size); ++ ++ return -ENOMEM; ++ } + + /* Copy the snapshot to the user buffer */ + ret = copy_from_user(me, buffer, snapshot_size); -+ BUG_ON(ret); ++ if (ret) { ++ printk("%s: Failed to retrieve the snapshot from user space\n", __func__); ++ dma_free_coherent(capsule_dev, snapshot_size, me, dma_handle); ++ ++ return -EFAULT; ++ } + + /* The snapshot is written in stages (issue #287) so that the calling + * CPU gets its interrupts back between two chunks of capsule memory. diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch index 30e7ac88a..c799c4c4b 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/core.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/core.c 2025-08-30 15:29:26.627707308 +0200 -@@ -0,0 +1,317 @@ +@@ -0,0 +1,324 @@ +/* + * Copyright (C) 2014-2019 Daniel Rossier + * Copyright (C) 2017, 2018 Baptiste Delporte @@ -122,6 +122,13 @@ + + case AGENCY_IOCTL_WRITE_SNAPSHOT: + args.slotID = write_snapshot(args.buffer); ++ ++ /* Let the caller see the failure: the snapshot may be rejected ++ * before any slot gets allocated. ++ */ ++ ++ if (args.slotID < 0) ++ ret = args.slotID; + break; + + case AGENCY_IOCTL_INJECT_CAPSULE: From 53772888b4ecec57d5e10a116b73bbf8e1eac332 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 11:07:00 +0200 Subject: [PATCH 02/10] soo: stream capsule snapshots through a bounce buffer Saving or restoring a capsule made the agency allocate a DMA buffer as large as the whole snapshot, i.e. the capsule slot plus its header. On virt64 that is 128 MB of physically contiguous memory taken from a 160 MB CMA zone, and the EMISO engine holds a copy of the same snapshot in user space at that very moment. cma_alloc() then has to migrate the movable pages sitting in the way, fails with -EBUSY, and the fallback to the buddy allocator asks for an order-16 block which cannot exist. The staged protocol already moves the capsule memory 4 MB at a time, so only one chunk is ever in flight: pass a bounce buffer of that size instead of the whole snapshot. AVZ now reads and writes each chunk at the beginning of the buffer, bounded by the size the agency advertises, and the agency copies it to or from user space as the stages progress. The buffer is reserved once, at init time, while the CMA zone is still pristine. The snapshot is no longer held twice in RAM, and neither path depends on a large contiguous allocation any more. --- .../soo/files/soo-generic/0062-soo.h.patch | 46 ++++- .../files/soo-generic/0073-capsule.c.patch | 181 ++++++++++++++---- .../soo/files/soo-generic/0077-core.c.patch | 9 +- so3/so3/avz/kernel/injector.c | 35 +++- so3/so3/soo/include/soo/uapi/soo.h | 16 +- 5 files changed, 231 insertions(+), 56 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch index 448e20a86..b737c4eb6 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/include/soo/uapi/soo.h 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/include/soo/uapi/soo.h 2025-08-29 08:46:41.395924678 +0200 -@@ -0,0 +1,482 @@ +@@ -0,0 +1,524 @@ +/* + * Copyright (C) 2014-2025 Daniel Rossier + * @@ -74,6 +74,13 @@ + +void do_gnttab(gnttab_op_t *args); + ++#define MAX_FBDEV_PFN 8 ++typedef struct fbdev_pfns { ++ size_t pfn_count; ++ addr_t pfn[MAX_FBDEV_PFN]; ++ size_t page_count[MAX_FBDEV_PFN]; ++} fbdev_pfns_t; ++ +#define AVZ_SCHEDULER_FLIP 0 + +/* @@ -302,6 +309,9 @@ +#define AVZ_SET_S3C_STATE 11 +#define AVZ_GET_DOM_DESC 12 +#define AVZ_GRANT_TABLE_OP 13 ++#define AVZ_FBDEV_SET_PFNS 14 ++#define AVZ_FBDEV_CHANGE_FOCUS 15 ++#define AVZ_FBDEV_GET_S3C_ADDR 16 + +/* Staged hypercalls. + * @@ -320,6 +330,14 @@ +#define AVZ_STAGE_CHUNK 1 +#define AVZ_STAGE_FINALIZE 2 + ++/* Maximum amount of capsule memory moved in a single AVZ_STAGE_CHUNK call. ++ * ++ * A snapshot is streamed through a bounce buffer of that size instead of being ++ * mapped as a whole: the agency has no reason to find several hundreds of ++ * contiguous MB in its CMA zone just to save or restore a capsule. ++ */ ++#define AVZ_STAGE_CHUNK_SIZE (4 * 1024 * 1024) ++ +/* AVZ_INJECT_CAPSULE */ +typedef struct { + void *itb_paddr; @@ -367,10 +385,16 @@ + +/* AVZ_READ_SNAPSHOT */ +/* AVZ_WRITE_SNAPSHOT */ ++/* ++ * `snapshot_paddr` points to the agency bounce buffer and is read again at each ++ * stage. At the INIT stage the buffer holds the snapshot header (payload size + ++ * domain context); at the CHUNK stage it holds one chunk of capsule memory, at ++ * its very beginning, and `size` tells AVZ how much of it may be used. ++ */ +typedef struct { + void *snapshot_paddr; + int32_t slotID; -+ int size; ++ int size; /* INIT: IN/OUT snapshot size / CHUNK: IN bounce buffer size */ + uint32_t stage; /* IN: AVZ_STAGE_* */ + uint32_t offset; /* CHUNK: IN/OUT byte cursor / INIT: OUT bytes to copy */ +} avz_snapshot_t; @@ -385,6 +409,21 @@ + gnttab_op_t gnttab_op; +} avz_gnttab_t; + ++/* AVZ_FBDEV_SET_PFNS */ ++typedef struct { ++ fbdev_pfns_t fbdev; ++} avz_fbdev_pfns_t; ++ ++/* AVZ_FBDEV_CHANGE_FOCUS */ ++typedef struct { ++ int new_slotID; ++} avz_fbdev_focus_t; ++ ++/* AVZ_FBDEV_GET_S3C_ADDR */ ++typedef struct { ++ addr_t paddr; ++} avz_fbdev_addr_t; ++ +/* + * AVZ hypercall argument + */ @@ -404,6 +443,9 @@ + avz_console_io_t avz_console_io_args; + avz_domctl_t avz_domctl_args; + avz_gnttab_t avz_gnttab_args; ++ avz_fbdev_pfns_t avz_fbdev_pfns_args; ++ avz_fbdev_focus_t avz_fbdev_focus_args; ++ avz_fbdev_addr_t avz_fbdev_addr_args; + } u; +} avz_hyp_t; + diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch index bd2bb3136..781386a5c 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/capsule.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/capsule.c 2025-08-30 15:37:09.704925348 +0200 -@@ -0,0 +1,651 @@ +@@ -0,0 +1,748 @@ +/* + * Copyright (C) 2017-2024 Daniel Rossier + * @@ -60,6 +60,16 @@ + +struct device *capsule_dev = NULL; + ++/* Bounce buffer used to stream capsule snapshots to and from AVZ, one ++ * AVZ_STAGE_CHUNK_SIZE chunk at a time. It is allocated once, at init time, ++ * while the CMA zone is still pristine: a snapshot no longer requires a ++ * contiguous region of its own size, which the agency could not reliably ++ * obtain any more once its memory had been used (capsule slots are hundreds ++ * of MB, and cma_alloc() has to migrate every movable page out of the way). ++ */ ++static void *snapshot_bounce = NULL; ++static dma_addr_t snapshot_bounce_handle; ++ +/* Serialize the staged injection hypercall sequence (issue #287). */ + +static DEFINE_MUTEX(inject_lock); @@ -196,10 +206,7 @@ + */ +void read_snapshot(uint32_t slotID, void *buffer, uint32_t *size) { + avz_hyp_t args; -+ void *me = NULL; -+ int ret; -+ uint32_t payload_size; -+ dma_addr_t dma_handle; ++ uint32_t payload_size, header_size, offset, chunk_size; + S3C_state_t S3C_state; + + /* No snapshot can be taken from a slot which does not host any capsule. */ @@ -241,15 +248,17 @@ + printk("### state: %d\n", get_S3C_state(slotID)); + } + -+ /* -+ * Prepare a buffer to store the S3C and additional header information like the snapshot structure. -+ */ ++ if (!snapshot_bounce) { ++ printk("%s: no snapshot bounce buffer available\n", __func__); ++ *size = 0; + -+ me = dma_alloc_coherent(capsule_dev, *size, &dma_handle, GFP_KERNEL); -+ BUG_ON(!me); ++ return; ++ } + + /* The snapshot is read in stages (issue #287) so that the calling CPU -+ * gets its interrupts back between two chunks of capsule memory. ++ * gets its interrupts back between two chunks of capsule memory. Each ++ * chunk transits through the bounce buffer and is handed over to user ++ * space right away, so the whole snapshot is never held twice in RAM. + */ + + mutex_lock(&inject_lock); @@ -257,28 +266,73 @@ + args.cmd = AVZ_S3C_READ_SNAPSHOT; + + args.u.avz_snapshot_args.slotID = slotID; -+ args.u.avz_snapshot_args.snapshot_paddr = (void *) dma_handle; ++ args.u.avz_snapshot_args.snapshot_paddr = (void *) snapshot_bounce_handle; + args.u.avz_snapshot_args.size = *size; + args.u.avz_snapshot_args.stage = AVZ_STAGE_INIT; + + avz_hypercall(&args); + -+ if (args.u.avz_snapshot_args.size == 0) ++ if (args.u.avz_snapshot_args.size == 0) { ++ *size = 0; ++ + goto out; ++ } + -+ /* INIT returns the amount of capsule memory to be copied. */ ++ /* INIT returns the amount of capsule memory to be copied in , ++ * the header it just left in the bounce buffer accounting for the rest. ++ */ + + payload_size = args.u.avz_snapshot_args.offset; + ++ if ((payload_size >= args.u.avz_snapshot_args.size) || ++ (args.u.avz_snapshot_args.size - payload_size > AVZ_STAGE_CHUNK_SIZE)) { ++ printk("%s: inconsistent snapshot header (size %d, payload %u)\n", __func__, ++ args.u.avz_snapshot_args.size, payload_size); ++ *size = 0; ++ ++ goto out; ++ } ++ ++ header_size = args.u.avz_snapshot_args.size - payload_size; ++ ++ if (copy_to_user(buffer, snapshot_bounce, header_size)) { ++ *size = 0; ++ ++ goto out; ++ } ++ + args.u.avz_snapshot_args.offset = 0; + + while (args.u.avz_snapshot_args.offset < payload_size) { ++ offset = args.u.avz_snapshot_args.offset; ++ + args.u.avz_snapshot_args.stage = AVZ_STAGE_CHUNK; ++ args.u.avz_snapshot_args.size = AVZ_STAGE_CHUNK_SIZE; + + avz_hypercall(&args); + -+ if (args.u.avz_snapshot_args.size == 0) ++ if (args.u.avz_snapshot_args.size == 0) { ++ *size = 0; ++ ++ goto out; ++ } ++ ++ /* AVZ advances the cursor by what it actually copied. */ ++ ++ chunk_size = args.u.avz_snapshot_args.offset - offset; ++ ++ if ((chunk_size == 0) || (chunk_size > AVZ_STAGE_CHUNK_SIZE)) { ++ printk("%s: invalid chunk of %u bytes at offset %u\n", __func__, chunk_size, offset); ++ *size = 0; ++ ++ goto out; ++ } ++ ++ if (copy_to_user(buffer + header_size + offset, snapshot_bounce, chunk_size)) { ++ *size = 0; ++ + goto out; ++ } + + cond_resched(); + } @@ -290,12 +344,6 @@ +out: + mutex_unlock(&inject_lock); + -+ /* Copy the snapshot to the user buffer */ -+ ret = copy_to_user(buffer, me, *size); -+ BUG_ON(ret); -+ -+ dma_free_coherent(capsule_dev, *size, me, dma_handle); -+ + S3C_state = get_S3C_state(slotID); + + if (S3C_state == S3C_state_resuming) { @@ -323,11 +371,9 @@ + */ +int write_snapshot(void *buffer) { + avz_hyp_t args; -+ void *me = NULL; -+ int ret; -+ dma_addr_t dma_handle; -+ uint32_t snapshot_size; -+ uint32_t payload_size; ++ int ret = 0; ++ uint32_t snapshot_size, header_size, head_len; ++ uint32_t payload_size, offset, chunk_size; + uint32_t slotID; + S3C_state_t S3C_state; + @@ -349,27 +395,29 @@ + return -EINVAL; + } + -+ /* -+ * Prepare a buffer to store the S3C and additional header information like the snapshot structure. -+ */ -+ me = dma_alloc_coherent(capsule_dev, snapshot_size, &dma_handle, GFP_KERNEL | __GFP_NOWARN); -+ if (!me) { -+ printk("%s: Failed to allocate %u bytes for the snapshot\n", __func__, snapshot_size); ++ if (!snapshot_bounce) { ++ printk("%s: no snapshot bounce buffer available\n", __func__); + + return -ENOMEM; + } + -+ /* Copy the snapshot to the user buffer */ -+ ret = copy_from_user(me, buffer, snapshot_size); -+ if (ret) { -+ printk("%s: Failed to retrieve the snapshot from user space\n", __func__); -+ dma_free_coherent(capsule_dev, snapshot_size, me, dma_handle); ++ /* AVZ reads the snapshot header (payload size + domain context) at the ++ * INIT stage: hand it over in the bounce buffer, which the CHUNK stages ++ * then reuse for the capsule memory itself. ++ */ ++ ++ head_len = (snapshot_size < AVZ_STAGE_CHUNK_SIZE ? snapshot_size : AVZ_STAGE_CHUNK_SIZE); ++ ++ if (copy_from_user(snapshot_bounce, buffer, head_len)) { ++ printk("%s: Failed to retrieve the snapshot header from user space\n", __func__); + + return -EFAULT; + } + + /* The snapshot is written in stages (issue #287) so that the calling + * CPU gets its interrupts back between two chunks of capsule memory. ++ * Each chunk transits through the bounce buffer, so the whole snapshot ++ * is never held twice in RAM. + */ + + mutex_lock(&inject_lock); @@ -378,7 +426,7 @@ + + args.u.avz_snapshot_args.size = snapshot_size; + args.u.avz_snapshot_args.slotID = -1; -+ args.u.avz_snapshot_args.snapshot_paddr = (void *) dma_handle; ++ args.u.avz_snapshot_args.snapshot_paddr = (void *) snapshot_bounce_handle; + args.u.avz_snapshot_args.stage = AVZ_STAGE_INIT; + + DBG("%s: Now asking AVZ to re-implement the snapshot of size %d bytes...\n", __func__, snapshot_size); @@ -392,20 +440,54 @@ + if (args.u.avz_snapshot_args.slotID < 2) + goto out; + -+ /* INIT returns the amount of capsule memory to be copied. */ ++ /* INIT returns the amount of capsule memory to be copied in , ++ * the header accounting for the rest of the snapshot. ++ */ + + payload_size = args.u.avz_snapshot_args.offset; + ++ if (payload_size >= snapshot_size) { ++ printk("%s: inconsistent snapshot (size %u, payload %u)\n", __func__, snapshot_size, payload_size); ++ args.u.avz_snapshot_args.slotID = -1; ++ ++ goto out; ++ } ++ ++ header_size = snapshot_size - payload_size; ++ + args.u.avz_snapshot_args.offset = 0; + + while (args.u.avz_snapshot_args.offset < payload_size) { ++ offset = args.u.avz_snapshot_args.offset; ++ chunk_size = payload_size - offset; ++ ++ if (chunk_size > AVZ_STAGE_CHUNK_SIZE) ++ chunk_size = AVZ_STAGE_CHUNK_SIZE; ++ ++ if (copy_from_user(snapshot_bounce, buffer + header_size + offset, chunk_size)) { ++ printk("%s: Failed to retrieve the snapshot from user space\n", __func__); ++ ret = -EFAULT; ++ ++ goto out; ++ } ++ + args.u.avz_snapshot_args.stage = AVZ_STAGE_CHUNK; ++ args.u.avz_snapshot_args.size = chunk_size; + + avz_hypercall(&args); + + if (args.u.avz_snapshot_args.slotID < 2) + goto out; + ++ /* AVZ advances the cursor by what it actually copied. */ ++ ++ if (args.u.avz_snapshot_args.offset <= offset) { ++ printk("%s: no progress at offset %u\n", __func__, offset); ++ args.u.avz_snapshot_args.slotID = -1; ++ ++ goto out; ++ } ++ + cond_resched(); + } + @@ -416,16 +498,17 @@ +out: + mutex_unlock(&inject_lock); + ++ if (ret) ++ return ret; ++ + if (args.u.avz_snapshot_args.slotID < 2) { + printk("%s: No free space...\n", __func__); -+ dma_free_coherent(capsule_dev, snapshot_size, me, dma_handle); ++ + return -1; /* No free space */ + } + + slotID = args.u.avz_snapshot_args.slotID; + -+ dma_free_coherent(capsule_dev, snapshot_size, me, dma_handle); -+ + S3C_state = get_S3C_state(slotID); + + if (S3C_state == S3C_state_stopped) { @@ -646,9 +729,23 @@ + capsule_dev = cma_malloc_miscdevice.this_device; + capsule_dev->coherent_dma_mask = DMA_BIT_MASK(64); + capsule_dev->dma_mask = &capsule_dev->coherent_dma_mask; ++ ++ /* Reserve the snapshot bounce buffer now, while the CMA zone is still ++ * free of any movable page which would have to be migrated away. ++ */ ++ ++ snapshot_bounce = dma_alloc_coherent(capsule_dev, AVZ_STAGE_CHUNK_SIZE, &snapshot_bounce_handle, GFP_KERNEL); ++ if (!snapshot_bounce) ++ printk("%s: failed to reserve the %d bytes snapshot bounce buffer\n", __func__, ++ AVZ_STAGE_CHUNK_SIZE); +} + +void capsule_exit(void) { ++ if (snapshot_bounce) { ++ dma_free_coherent(capsule_dev, AVZ_STAGE_CHUNK_SIZE, snapshot_bounce, snapshot_bounce_handle); ++ snapshot_bounce = NULL; ++ } ++ + misc_deregister(&cma_malloc_miscdevice); + capsule_dev = NULL; +} diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch index c799c4c4b..d1c70a529 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/core.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/core.c 2025-08-30 15:29:26.627707308 +0200 -@@ -0,0 +1,324 @@ +@@ -0,0 +1,331 @@ +/* + * Copyright (C) 2014-2019 Daniel Rossier + * Copyright (C) 2017, 2018 Baptiste Delporte @@ -118,6 +118,13 @@ + + case AGENCY_IOCTL_READ_SNAPSHOT: + read_snapshot(args.slotID, args.buffer, (uint32_t *)&args.value); ++ ++ /* A size of zero means no snapshot could be read: the caller must ++ * not store what it was given. ++ */ ++ ++ if (args.value == 0) ++ ret = -EIO; + break; + + case AGENCY_IOCTL_WRITE_SNAPSHOT: diff --git a/so3/so3/avz/kernel/injector.c b/so3/so3/avz/kernel/injector.c index 4406e2f72..a5547b656 100644 --- a/so3/so3/avz/kernel/injector.c +++ b/so3/so3/avz/kernel/injector.c @@ -47,20 +47,27 @@ static struct dom_context domain_context = { 0 }; /* Maximum amount of domain memory moved in one AVZ_STAGE_CHUNK call. * This bounds the time spent at EL2 with IRQs off on the calling CPU - * (issue #287). + * (issue #287). It is shared with the agency, which sizes its snapshot + * bounce buffer accordingly. */ -#define STAGE_CHUNK_SIZE (4 * SZ_1M) +#define STAGE_CHUNK_SIZE AVZ_STAGE_CHUNK_SIZE /** * Compute the size of the next chunk to be moved, and advance the cursor. + * + * @max further bounds the chunk to what the agency buffer can hold; pass 0 when + * no agency buffer is involved (the injection clears the slot in place). */ -static size_t stage_next_chunk(uint32_t *offset, size_t total) +static size_t stage_next_chunk(uint32_t *offset, size_t total, size_t max) { size_t chunk_size = total - *offset; if (chunk_size > STAGE_CHUNK_SIZE) chunk_size = STAGE_CHUNK_SIZE; + if (max && (chunk_size > max)) + chunk_size = max; + *offset += chunk_size; return chunk_size; @@ -159,7 +166,7 @@ void inject_capsule(avz_hyp_t *args) /* Clear the next chunk of the RAM allocated to this capsule */ - chunk_size = stage_next_chunk(&args->u.avz_inject_capsule_args.offset, memslot[slotID].size); + chunk_size = stage_next_chunk(&args->u.avz_inject_capsule_args.offset, memslot[slotID].size, 0); memset((void *) __xva(slotID, memslot[slotID].base_paddr + offset), 0, chunk_size); @@ -392,10 +399,14 @@ void read_S3C_snapshot(avz_hyp_t *args) return; } - chunk_size = stage_next_chunk(&args->u.avz_snapshot_args.offset, memslot[slotID].size); + chunk_size = stage_next_chunk(&args->u.avz_snapshot_args.offset, memslot[slotID].size, + args->u.avz_snapshot_args.size); + + /* The chunk is handed over at the beginning of the bounce buffer; + * placing it in the snapshot is up to the agency. + */ - memcpy(snapshot_buffer + payload_offset + offset, (void *) __xva(slotID, memslot[slotID].base_paddr + offset), - chunk_size); + memcpy(snapshot_buffer, (void *) __xva(slotID, memslot[slotID].base_paddr + offset), chunk_size); break; @@ -560,10 +571,14 @@ void write_S3C_snapshot(avz_hyp_t *args) /* Copy the next chunk of the capsule content */ - chunk_size = stage_next_chunk(&args->u.avz_snapshot_args.offset, memslot[slotID].size); + chunk_size = stage_next_chunk(&args->u.avz_snapshot_args.offset, memslot[slotID].size, + args->u.avz_snapshot_args.size); + + /* The agency has placed the chunk at the beginning of the bounce + * buffer, whatever its position in the snapshot. + */ - memcpy((void *) __xva(slotID, memslot[slotID].base_paddr + offset), snapshot_buffer + payload_offset + offset, - chunk_size); + memcpy((void *) __xva(slotID, memslot[slotID].base_paddr + offset), snapshot_buffer, chunk_size); return; } diff --git a/so3/so3/soo/include/soo/uapi/soo.h b/so3/so3/soo/include/soo/uapi/soo.h index 008e2beaa..db1d2c202 100644 --- a/so3/so3/soo/include/soo/uapi/soo.h +++ b/so3/so3/soo/include/soo/uapi/soo.h @@ -334,6 +334,14 @@ typedef struct agency_ioctl_args { #define AVZ_STAGE_CHUNK 1 #define AVZ_STAGE_FINALIZE 2 +/* Maximum amount of capsule memory moved in a single AVZ_STAGE_CHUNK call. + * + * A snapshot is streamed through a bounce buffer of that size instead of being + * mapped as a whole: the agency has no reason to find several hundreds of + * contiguous MB in its CMA zone just to save or restore a capsule. + */ +#define AVZ_STAGE_CHUNK_SIZE (4 * 1024 * 1024) + /* AVZ_INJECT_CAPSULE */ typedef struct { void *itb_paddr; @@ -381,10 +389,16 @@ typedef struct { /* AVZ_READ_SNAPSHOT */ /* AVZ_WRITE_SNAPSHOT */ +/* + * `snapshot_paddr` points to the agency bounce buffer and is read again at each + * stage. At the INIT stage the buffer holds the snapshot header (payload size + + * domain context); at the CHUNK stage it holds one chunk of capsule memory, at + * its very beginning, and `size` tells AVZ how much of it may be used. + */ typedef struct { void *snapshot_paddr; int32_t slotID; - int size; + int size; /* INIT: IN/OUT snapshot size / CHUNK: IN bounce buffer size */ uint32_t stage; /* IN: AVZ_STAGE_* */ uint32_t offset; /* CHUNK: IN/OUT byte cursor / INIT: OUT bytes to copy */ } avz_snapshot_t; From 15397baf4fbc1d536a7a5cc1ede16dbdbb413a05 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 11:21:15 +0200 Subject: [PATCH 03/10] soo: validate the capsule image handed to inject_capsule() The injection path had the same weaknesses as the snapshot one: the size came straight from user space with no bound, dma_alloc_coherent() was followed by a BUG_ON(), and the image itself was read with a plain memcpy() from a user pointer. Reject an implausible size, report -ENOMEM instead of dying when the CMA zone cannot serve the request, copy the image with copy_from_user(), and propagate the failure through the INJECT_CAPSULE ioctl. The staged loop also gives up if AVZ stops advancing the cursor, as on the snapshot paths. The ITB is parsed as a whole by AVZ, so unlike a snapshot it still needs one contiguous allocation -- a few MB, taken once per injection. --- .../files/soo-generic/0073-capsule.c.patch | 48 ++++++++++++++++--- .../soo/files/soo-generic/0077-core.c.patch | 9 +++- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch index 781386a5c..04fab53d6 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/capsule.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/capsule.c 2025-08-30 15:37:09.704925348 +0200 -@@ -0,0 +1,748 @@ +@@ -0,0 +1,782 @@ +/* + * Copyright (C) 2017-2024 Daniel Rossier + * @@ -102,15 +102,38 @@ + void *me = NULL; + dma_addr_t dma_handle; + avz_hyp_t args; -+ uint32_t slot_size; ++ uint32_t slot_size, offset; + -+ DBG("Original contents at address: 0x%08x\n with size %d bytes\n", (unsigned long) buffer, size); ++ DBG("Original contents at address: %p\n with size %zu bytes\n", buffer, size); + -+ /* Allocate a contiguous memory region to host the S3C */ -+ me = dma_alloc_coherent(capsule_dev, size, &dma_handle, GFP_KERNEL); -+ BUG_ON(!me); ++ /* The size comes from user space along with the buffer it describes: ++ * reject anything the agency could not hold anyway, rather than turning ++ * it into an allocation request. ++ */ ++ ++ if ((size == 0) || (size > (totalram_pages() << PAGE_SHIFT))) { ++ printk("%s: Invalid capsule size (%zu bytes)\n", __func__, size); ++ ++ return -EINVAL; ++ } ++ ++ /* Allocate a contiguous memory region to host the S3C. AVZ parses the ++ * ITB as a whole, so this one cannot be streamed the way a snapshot is. ++ */ ++ ++ me = dma_alloc_coherent(capsule_dev, size, &dma_handle, GFP_KERNEL | __GFP_NOWARN); ++ if (!me) { ++ printk("%s: Failed to allocate %zu contiguous bytes for the capsule\n", __func__, size); ++ ++ return -ENOMEM; ++ } + -+ memcpy(me, buffer, size); ++ if (copy_from_user(me, buffer, size)) { ++ printk("%s: Failed to retrieve the capsule from user space\n", __func__); ++ dma_free_coherent(capsule_dev, size, me, dma_handle); ++ ++ return -EFAULT; ++ } + + /* Since the S3C buffer is in the CMA zone and allocated via the + * dma_alloc_cohenrent() function, we cannot use virt_to_phys(). So, we @@ -147,6 +170,8 @@ + args.u.avz_inject_capsule_args.offset = 0; + + while (args.u.avz_inject_capsule_args.offset < slot_size) { ++ offset = args.u.avz_inject_capsule_args.offset; ++ + args.u.avz_inject_capsule_args.stage = AVZ_STAGE_CHUNK; + + avz_hypercall(&args); @@ -154,6 +179,15 @@ + if (args.u.avz_inject_capsule_args.slotID < 0) + goto out; + ++ /* AVZ advances the cursor by what it actually cleared. */ ++ ++ if (args.u.avz_inject_capsule_args.offset <= offset) { ++ printk("%s: no progress at offset %u\n", __func__, offset); ++ args.u.avz_inject_capsule_args.slotID = -1; ++ ++ goto out; ++ } ++ + cond_resched(); + } + diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch index d1c70a529..744a06bc1 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/core.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/core.c 2025-08-30 15:29:26.627707308 +0200 -@@ -0,0 +1,331 @@ +@@ -0,0 +1,338 @@ +/* + * Copyright (C) 2014-2019 Daniel Rossier + * Copyright (C) 2017, 2018 Baptiste Delporte @@ -140,6 +140,13 @@ + + case AGENCY_IOCTL_INJECT_CAPSULE: + args.slotID = inject_capsule(args.buffer, args.value, args.slotID, args.capsuleID); ++ ++ /* Let the caller see the failure: no slot could be allocated, or ++ * the capsule image was rejected before that. ++ */ ++ ++ if (args.slotID < 0) ++ ret = args.slotID; + break; + + case AGENCY_IOCTL_START_CAPSULE: From 936fa646cea8851ce73ae0d72ab69c986c9f364c Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 12:56:20 +0200 Subject: [PATCH 04/10] soo: never route the console input to a capsule which cannot take it The console focus is a plain index over the slots, so it keeps pointing at a capsule which is being snapshotted or has been shut down, and every keystroke is then handed to s3c_cons_sendc() for that slot. That path is not prepared for it. It runs from the serial interrupt and goes through vdevback_processing_begin(), which waits for the frontend to be connected: a capsule suspended for a snapshot, resuming, or gone has none, so the keystroke slept in the interrupt and hung the console for good. get_console() returning NULL for a vanished domain was dereferenced right away as well. Skip, when cycling the focus, every slot whose capsule is not living -- any other state is a capsule in transit or gone. Bail out of the send path when no console device answers for the domain, and take the new non-blocking vdevback_processing_try_begin(), which reports the frontend is not usable rather than waiting for it. vdevfront_is_connected() also read the device private data before testing the pointer for NULL; do it the other way round. --- .../files/soo-generic/0020-vdevback.c.patch | 47 ++++++++++++- .../soo/files/soo-generic/0032-vuart.c.patch | 15 +++- .../files/soo-generic/0065-vdevback.h.patch | 3 +- .../files/soo-generic/0074-console.c.patch | 70 +++++++++++++++++-- 4 files changed, 122 insertions(+), 13 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0020-vdevback.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0020-vdevback.c.patch index 69e4e37d3..4495aaff0 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0020-vdevback.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0020-vdevback.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/drivers/vdevback.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/drivers/vdevback.c 2025-08-29 08:46:41.395627342 +0200 -@@ -0,0 +1,309 @@ +@@ -0,0 +1,350 @@ + +/* + * Copyright (C) 2020 Daniel Rossier @@ -81,6 +81,46 @@ +} + +/* ++ * Non-blocking counterpart of vdevback_processing_begin(): it never waits for ++ * the frontend to (re)connect and reports false instead. ++ * ++ * Any caller running in a context which must not sleep -- the console input ++ * path, driven by the serial interrupt -- has to use this one. A capsule being ++ * suspended, snapshotted or shut down leaves its frontend out of the connected ++ * state, and waiting for it there would hang the interrupt. ++ */ ++bool vdevback_processing_try_begin(struct vbus_device *vdev) ++{ ++ void *priv = dev_get_drvdata(&vdev->dev); ++ vdevback_t *vdevback = (vdevback_t *) priv; ++ ++ if (vdev->fe_state != VbusStateConnected) ++ return false; ++ ++ if (atomic_read(&vdevback->processing_count) == 0) { ++ atomic_inc(&vdevback->processing_count); ++ ++ if (!mutex_trylock(&vdevback->processing_lock)) { ++ atomic_dec(&vdevback->processing_count); ++ ++ return false; ++ } ++ ++ /* Make sure we are still connected after taking the lock. */ ++ ++ if (vdev->fe_state != VbusStateConnected) { ++ mutex_unlock(&vdevback->processing_lock); ++ atomic_dec(&vdevback->processing_count); ++ ++ return false; ++ } ++ } else ++ atomic_inc(&vdevback->processing_count); ++ ++ return true; ++} ++ ++/* + * Finish a processing section against suspend/close prevention + */ +void vdevback_processing_end(struct vbus_device *vdev) { @@ -98,12 +138,13 @@ + * Check if the frontend state and tell whether it is connected. + */ +bool vdevfront_is_connected(struct vbus_device *vdev) { -+ void *priv = dev_get_drvdata(&vdev->dev); -+ vdevback_t *vdevback = (vdevback_t *) priv; ++ vdevback_t *vdevback; + + if (!vdev) + return false; + ++ vdevback = (vdevback_t *) dev_get_drvdata(&vdev->dev); ++ + return vdevback->vdevfront_connected; +} + diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0032-vuart.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0032-vuart.c.patch index ad9093147..f1270f559 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0032-vuart.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0032-vuart.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/drivers/vuartback/vuart.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/drivers/vuartback/vuart.c 2025-08-29 08:46:41.395804736 +0200 -@@ -0,0 +1,327 @@ +@@ -0,0 +1,336 @@ +/* + * Copyright (C) 2016-2018 Daniel Rossier + * Copyright (C) 2016 Baptiste Delporte @@ -191,10 +191,19 @@ + + console = get_console(domid); + -+ if (!vdevfront_is_connected(console)) ++ /* The capsule may have gone while the console focus was still pointing at ++ * its slot -- a paused capsule is suspended, then shut down. ++ */ ++ ++ if (!console || !vdevfront_is_connected(console)) + return; + -+ vdevback_processing_begin(console); ++ /* This runs from the serial interrupt: the blocking flavour would wait for ++ * a frontend which is suspended or already gone, and hang the console. ++ */ ++ ++ if (!vdevback_processing_try_begin(console)) ++ return; + + vuart_priv = dev_get_drvdata(&console->dev); + diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0065-vdevback.h.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0065-vdevback.h.patch index a257a278d..d2d1d9d3b 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0065-vdevback.h.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0065-vdevback.h.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/include/soo/vdevback.h 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/include/soo/vdevback.h 2025-08-29 08:46:41.396178895 +0200 -@@ -0,0 +1,90 @@ +@@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 Daniel Rossier + * @@ -72,6 +72,7 @@ + +void vdevback_init(char *name, vdrvback_t *vdrvback); +bool vdevback_processing_begin(struct vbus_device *vdev); ++bool vdevback_processing_try_begin(struct vbus_device *vdev); +void vdevback_processing_end(struct vbus_device *vdev); + +bool vdevfront_is_connected(struct vbus_device *vdev); diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0074-console.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0074-console.c.patch index b4270b411..0c408acda 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0074-console.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0074-console.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/console/console.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/console/console.c 2025-08-29 08:46:41.394716106 +0200 -@@ -0,0 +1,152 @@ +@@ -0,0 +1,210 @@ +/* + * Copyright (C) 2016-2019 Daniel Rossier + * @@ -32,6 +32,7 @@ + +#include +#include ++#include +#include + +/* 1 is the code for CTRL-A */ @@ -39,6 +40,14 @@ + +#define N_SWITCH_FOCUS 8 + ++/* Focus targets, in the order of below: the agency, the reserved ++ * slot, the capsule slots, and the hypervisor. ++ */ ++#define FOCUS_AGENCY 0 ++#define FOCUS_RESERVED 1 ++#define FOCUS_S3C_FIRST 2 ++#define FOCUS_S3C_LAST (N_SWITCH_FOCUS - 2) ++ +/* Volatile to ensure the value is read from memory and no optimization occurs */ +static int volatile avzcons_active = 0; +static int volatile vfb_active = 0; @@ -51,6 +60,45 @@ + return avzcons_active; +} + ++/** ++ * Tell whether the console input may be routed to this focus target. ++ * ++ * The agency and the hypervisor always qualify, the reserved slot never does, ++ * and a capsule slot only while its capsule is living. Every other state is a ++ * capsule which is gone or in transit -- being injected, suspended for a ++ * snapshot, resuming, or shut down -- and its vUART frontend is then not ++ * connected, so the keystrokes would have nowhere to go. ++ */ ++static bool console_focus_available(int focus) ++{ ++ if (focus == FOCUS_RESERVED) ++ return false; ++ ++ if ((focus < FOCUS_S3C_FIRST) || (focus > FOCUS_S3C_LAST)) ++ return true; ++ ++ return (get_S3C_state(focus) == S3C_state_living); ++} ++ ++/** ++ * Find the next focus target after @focus, skipping those which cannot take the ++ * input. The agency always qualifies, so the scan always ends somewhere. ++ */ ++static int console_next_focus(int focus, u32 focus_max) ++{ ++ int candidate = focus; ++ int scanned; ++ ++ for (scanned = 0; scanned <= focus_max; scanned++) { ++ candidate = (candidate + 1) % (focus_max + 1); ++ ++ if (console_focus_available(candidate)) ++ return candidate; ++ } ++ ++ return FOCUS_AGENCY; ++} ++ +int avzcons_set_focus(int next_domain) +{ + avzcons_active = next_domain; @@ -104,11 +152,8 @@ + /* We eat CTRL- in groups of 2 to switch console input. */ + if (++switch_code_count == 1) { + -+ active = (avzcons_get_focus() + 1) % (focus_max + 1); -+ active = ((active == 1) ? active+1 : active); -+ -+ next = (active + 1) % (focus_max + 1); -+ next = ((next == 1) ? next+1 : next); ++ active = console_next_focus(avzcons_get_focus(), focus_max); ++ next = console_next_focus(active, focus_max); + + avzcons_set_focus(active); + @@ -122,6 +167,19 @@ + } else { + switch_code_count = 0; + ++ /* The focused capsule may have gone in the meantime -- taking a ++ * snapshot of it shuts it down. Hand the input back to the agency ++ * instead of dropping every keystroke into a slot nobody serves. ++ */ ++ ++ if (!console_focus_available(avzcons_get_focus())) { ++ avzcons_set_focus(FOCUS_AGENCY); ++ ++ lprintk("*** The capsule is gone, serial input -> %s.\n", input_str[FOCUS_AGENCY]); ++ ++ return 0; ++ } ++ + switch (avzcons_get_focus()) { + default: + case 0: /* Input to the agency */ From 5c2d721892e485b413c3e4a853b3b220160a228b Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 13:19:56 +0200 Subject: [PATCH 05/10] soo: let a snapshot leave the capsule suspended Snapshotting a capsule suspends it, reads its memory and resumes it: a capsule which was living stays living, which is the whole point of snapshotting a running one. A caller which shuts the capsule down right after -- pausing it, in the EMISO engine -- gets the opposite of what it wants, though: the capsule is woken up only to be killed, and runs for a moment, diverging from the snapshot just taken. Add an AVZ_STAGE_FINALIZE_HOLD stage, which completes the snapshot and leaves the capsule suspended, and an AGENCY_IOCTL_READ_SNAPSHOT_HOLD reaching it. AGENCY_IOCTL_READ_SNAPSHOT is untouched and still resumes. shutdown_capsule() has to know about that state: it asks the capsule to terminate through the DC_SHUTDOWN handshake, and do_sync_dom() waits for the answer without any deadline. A suspended capsule is not scheduled any more and can never answer, exactly like the stopped and killed ones the handshake is already skipped for, so reap it directly as well. While at it, a snapshot which fails halfway no longer leaves the capsule suspended for good: every error path now goes through the resuming stage, whichever flavour was asked for. --- .../files/soo-generic/0036-capsule.h.patch | 5 +- .../soo/files/soo-generic/0062-soo.h.patch | 15 +++++- .../files/soo-generic/0073-capsule.c.patch | 53 ++++++++++--------- .../soo/files/soo-generic/0077-core.c.patch | 35 ++++++++---- .../0001-usr-linux-1.0-r0/0012-soo.h.patch | 7 ++- so3/so3/avz/kernel/injector.c | 11 ++++ so3/so3/soo/include/soo/uapi/soo.h | 8 +++ 7 files changed, 93 insertions(+), 41 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0036-capsule.h.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0036-capsule.h.patch index 864d8cd68..3fd830f43 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0036-capsule.h.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0036-capsule.h.patch @@ -25,7 +25,7 @@ +int inject_capsule(void *buffer, size_t size, int slotID, unsigned capsuleId); +void start_capsule(int slotID); + -+void read_snapshot(uint32_t slotID, void *buffer, uint32_t *size); ++void read_snapshot(uint32_t slotID, void *buffer, uint32_t *size, bool resume); +int write_snapshot(void *buffer); + +int get_S3C_state(unsigned int S3C_slotID); @@ -40,5 +40,4 @@ +void capsule_init(void); +void capsule_exit(void); + -+#endif /* CAPSULE_H */ -\ No newline at end of file ++#endif /* CAPSULE_H */ \ No newline at end of file diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch index b737c4eb6..9c74310e1 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/include/soo/uapi/soo.h 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/include/soo/uapi/soo.h 2025-08-29 08:46:41.395924678 +0200 -@@ -0,0 +1,524 @@ +@@ -0,0 +1,537 @@ +/* + * Copyright (C) 2014-2025 Daniel Rossier + * @@ -174,6 +174,11 @@ +#define AGENCY_IOCTL_GET_S3C_ID _IOWR('S', 6, agency_ioctl_args_t) +#define AGENCY_IOCTL_GET_S3C_ID_ARRAY _IOR('S', 7, agency_ioctl_args_t) + ++/* Same as AGENCY_IOCTL_READ_SNAPSHOT, but the capsule is left suspended instead ++ * of being resumed: for a caller which shuts it down right after. ++ */ ++#define AGENCY_IOCTL_READ_SNAPSHOT_HOLD _IOWR('S', 8, agency_ioctl_args_t) ++ +#define SOO_NAME_SIZE 16 + +/* @@ -330,6 +335,14 @@ +#define AVZ_STAGE_CHUNK 1 +#define AVZ_STAGE_FINALIZE 2 + ++/* Snapshot only: complete without resuming the capsule, which is left ++ * suspended. Taking a snapshot normally leaves the capsule living -- that is ++ * the point of snapshotting a running capsule -- but a caller which is about to ++ * shut it down would otherwise let it run, and diverge from the snapshot it has ++ * just taken, for nothing. ++ */ ++#define AVZ_STAGE_FINALIZE_HOLD 3 ++ +/* Maximum amount of capsule memory moved in a single AVZ_STAGE_CHUNK call. + * + * A snapshot is streamed through a bounce buffer of that size instead of being diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch index 04fab53d6..81dfab632 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/capsule.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/capsule.c 2025-08-30 15:37:09.704925348 +0200 -@@ -0,0 +1,782 @@ +@@ -0,0 +1,783 @@ +/* + * Copyright (C) 2017-2024 Daniel Rossier + * @@ -238,7 +238,7 @@ + * @param slotID + * @param buffer pointer to the S3C buffer + */ -+void read_snapshot(uint32_t slotID, void *buffer, uint32_t *size) { ++void read_snapshot(uint32_t slotID, void *buffer, uint32_t *size, bool resume) { + avz_hyp_t args; + uint32_t payload_size, header_size, offset, chunk_size; + S3C_state_t S3C_state; @@ -306,11 +306,8 @@ + + avz_hypercall(&args); + -+ if (args.u.avz_snapshot_args.size == 0) { -+ *size = 0; -+ -+ goto out; -+ } ++ if (args.u.avz_snapshot_args.size == 0) ++ goto fail; + + /* INIT returns the amount of capsule memory to be copied in , + * the header it just left in the bounce buffer accounting for the rest. @@ -322,18 +319,14 @@ + (args.u.avz_snapshot_args.size - payload_size > AVZ_STAGE_CHUNK_SIZE)) { + printk("%s: inconsistent snapshot header (size %d, payload %u)\n", __func__, + args.u.avz_snapshot_args.size, payload_size); -+ *size = 0; + -+ goto out; ++ goto fail; + } + + header_size = args.u.avz_snapshot_args.size - payload_size; + -+ if (copy_to_user(buffer, snapshot_bounce, header_size)) { -+ *size = 0; -+ -+ goto out; -+ } ++ if (copy_to_user(buffer, snapshot_bounce, header_size)) ++ goto fail; + + args.u.avz_snapshot_args.offset = 0; + @@ -345,11 +338,8 @@ + + avz_hypercall(&args); + -+ if (args.u.avz_snapshot_args.size == 0) { -+ *size = 0; -+ -+ goto out; -+ } ++ if (args.u.avz_snapshot_args.size == 0) ++ goto fail; + + /* AVZ advances the cursor by what it actually copied. */ + @@ -357,20 +347,31 @@ + + if ((chunk_size == 0) || (chunk_size > AVZ_STAGE_CHUNK_SIZE)) { + printk("%s: invalid chunk of %u bytes at offset %u\n", __func__, chunk_size, offset); -+ *size = 0; + -+ goto out; ++ goto fail; + } + -+ if (copy_to_user(buffer + header_size + offset, snapshot_bounce, chunk_size)) { -+ *size = 0; -+ -+ goto out; -+ } ++ if (copy_to_user(buffer + header_size + offset, snapshot_bounce, chunk_size)) ++ goto fail; + + cond_resched(); + } + ++ /* The capsule is resumed, unless the caller is about to shut it down. */ ++ ++ args.u.avz_snapshot_args.stage = (resume ? AVZ_STAGE_FINALIZE : AVZ_STAGE_FINALIZE_HOLD); ++ ++ avz_hypercall(&args); ++ ++ goto out; ++ ++fail: ++ *size = 0; ++ ++ /* The capsule was suspended before the first stage: bring it back ++ * whatever went wrong, rather than leaving it frozen for good. ++ */ ++ + args.u.avz_snapshot_args.stage = AVZ_STAGE_FINALIZE; + + avz_hypercall(&args); diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch index 744a06bc1..6a1a71546 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/core.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/core.c 2025-08-30 15:29:26.627707308 +0200 -@@ -0,0 +1,338 @@ +@@ -0,0 +1,353 @@ +/* + * Copyright (C) 2014-2019 Daniel Rossier + * Copyright (C) 2017, 2018 Baptiste Delporte @@ -59,28 +59,36 @@ +static int shutdown_capsule(unsigned int S3C_slotID) +{ + avz_hyp_t args; ++ S3C_state_t S3C_state; + + /* Nothing to do if the slot does not host any capsule; a DC event + * sent to an empty slot has no possible recipient. + */ + -+ if (get_S3C_state(S3C_slotID) == S3C_state_dead) { ++ S3C_state = get_S3C_state(S3C_slotID); ++ ++ if (S3C_state == S3C_state_dead) { + lprintk("%s: no capsule in slot %d, nothing to shut down.\n", __func__, S3C_slotID); + return -ESRCH; + } + -+ /* Asynchronous termination of the capsule. A crashed capsule (killed -+ * by AVZ after a fault) cannot answer the DC_SHUTDOWN handshake -+ * anymore: it is reaped directly. ++ /* Asynchronous termination of the capsule, except for the states in which ++ * it cannot answer the DC_SHUTDOWN handshake: stopped (injected, never ++ * started), killed (faulted, reaped by AVZ) and suspended (held after a ++ * snapshot, hence not scheduled any more). do_sync_dom() waits for that ++ * answer without any deadline, so those are reaped directly. + */ + -+ if ((get_S3C_state(S3C_slotID) != S3C_state_stopped) && (get_S3C_state(S3C_slotID) != S3C_state_killed)) ++ if ((S3C_state != S3C_state_stopped) && (S3C_state != S3C_state_killed) && ++ (S3C_state != S3C_state_suspended)) { + do_sync_dom(S3C_slotID, DC_SHUTDOWN); + ++ S3C_state = get_S3C_state(S3C_slotID); ++ } ++ + /* Then, final termination of the residual S3C */ -+ if ((get_S3C_state(S3C_slotID) == S3C_state_terminated) || -+ (get_S3C_state(S3C_slotID) == S3C_state_stopped) || -+ (get_S3C_state(S3C_slotID) == S3C_state_killed)) { ++ if ((S3C_state == S3C_state_terminated) || (S3C_state == S3C_state_stopped) || ++ (S3C_state == S3C_state_killed) || (S3C_state == S3C_state_suspended)) { + args.cmd = AVZ_KILL_S3C; + args.u.avz_kill_me_args.slotID = S3C_slotID; + @@ -117,7 +125,14 @@ + break; + + case AGENCY_IOCTL_READ_SNAPSHOT: -+ read_snapshot(args.slotID, args.buffer, (uint32_t *)&args.value); ++ case AGENCY_IOCTL_READ_SNAPSHOT_HOLD: ++ ++ /* A capsule keeps running once it has been snapshotted, unless the ++ * caller asked to hold it suspended because it shuts it down next. ++ */ ++ ++ read_snapshot(args.slotID, args.buffer, (uint32_t *)&args.value, ++ (cmd == AGENCY_IOCTL_READ_SNAPSHOT)); + + /* A size of zero means no snapshot could be read: the caller must + * not store what it was given. diff --git a/build/meta-usr/recipes-usr/soo/files/0001-usr-linux-1.0-r0/0012-soo.h.patch b/build/meta-usr/recipes-usr/soo/files/0001-usr-linux-1.0-r0/0012-soo.h.patch index 5f1b204e1..49ec35038 100644 --- a/build/meta-usr/recipes-usr/soo/files/0001-usr-linux-1.0-r0/0012-soo.h.patch +++ b/build/meta-usr/recipes-usr/soo/files/0001-usr-linux-1.0-r0/0012-soo.h.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/micofe/build/tmp/work/usr-linux-1.0-r0/usr-linux-1.0/include/soo/uapi/soo.h 1970-01-01 01:00:00.000000000 +0100 +++ ./include/soo/uapi/soo.h 2025-10-10 17:28:33.584815499 +0200 -@@ -0,0 +1,98 @@ +@@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014-2025 Daniel Rossier + * @@ -90,6 +90,11 @@ +#define AGENCY_IOCTL_GET_S3C_ID _IOWR('S', 6, agency_ioctl_args_t) +#define AGENCY_IOCTL_GET_S3C_ID_ARRAY _IOR('S', 7, agency_ioctl_args_t) + ++/* Same as AGENCY_IOCTL_READ_SNAPSHOT, but the capsule is left suspended instead ++ * of being resumed: for a caller which shuts it down right after. ++ */ ++#define AGENCY_IOCTL_READ_SNAPSHOT_HOLD _IOWR('S', 8, agency_ioctl_args_t) ++ +/* struct agency_ioctl_args used in IOCTLs */ +typedef struct agency_ioctl_args { + void *buffer; /* IN/OUT */ diff --git a/so3/so3/avz/kernel/injector.c b/so3/so3/avz/kernel/injector.c index a5547b656..5c3a817c3 100644 --- a/so3/so3/avz/kernel/injector.c +++ b/so3/so3/avz/kernel/injector.c @@ -411,6 +411,17 @@ void read_S3C_snapshot(avz_hyp_t *args) break; case AVZ_STAGE_FINALIZE: + case AVZ_STAGE_FINALIZE_HOLD: + + /* A snapshot leaves the capsule living: it is suspended for the time + * the memory is read, then resumed. The HOLD variant skips that, for + * an agency which shuts the capsule down right after -- resuming it + * only to kill it would let it run, and diverge from the snapshot + * just taken, for nothing. + */ + + if (args->u.avz_snapshot_args.stage == AVZ_STAGE_FINALIZE_HOLD) + break; if (dom_S3C->avz_shared->dom_desc.u.S3C.state == S3C_state_suspended) { /* Now, this capsule is suspended and must be resumed by the agency */ diff --git a/so3/so3/soo/include/soo/uapi/soo.h b/so3/so3/soo/include/soo/uapi/soo.h index db1d2c202..7fee5cebc 100644 --- a/so3/so3/soo/include/soo/uapi/soo.h +++ b/so3/so3/soo/include/soo/uapi/soo.h @@ -334,6 +334,14 @@ typedef struct agency_ioctl_args { #define AVZ_STAGE_CHUNK 1 #define AVZ_STAGE_FINALIZE 2 +/* Snapshot only: complete without resuming the capsule, which is left + * suspended. Taking a snapshot normally leaves the capsule living -- that is + * the point of snapshotting a running capsule -- but a caller which is about to + * shut it down would otherwise let it run, and diverge from the snapshot it has + * just taken, for nothing. + */ +#define AVZ_STAGE_FINALIZE_HOLD 3 + /* Maximum amount of capsule memory moved in a single AVZ_STAGE_CHUNK call. * * A snapshot is streamed through a bounce buffer of that size instead of being From 257d0ac8606f3b91bff8bd5354845282f4612ef6 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 13:30:21 +0200 Subject: [PATCH 06/10] soo: drop the leftover state trace from read_snapshot Every snapshot of a living capsule printed its state on the console, which is debug output that outlived its purpose. --- .../recipes-linux/soo/files/soo-generic/0073-capsule.c.patch | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch index 81dfab632..775e950a9 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/capsule.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/capsule.c 2025-08-30 15:37:09.704925348 +0200 -@@ -0,0 +1,783 @@ +@@ -0,0 +1,781 @@ +/* + * Copyright (C) 2017-2024 Daniel Rossier + * @@ -278,8 +278,6 @@ + vbus_suspend_devices(slotID); + + do_sync_dom(slotID, DC_SUSPEND); -+ -+ printk("### state: %d\n", get_S3C_state(slotID)); + } + + if (!snapshot_bounce) { From 065aaa86cdb0091fea37cccc4fc2e9eec53c1e51 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 14:13:06 +0200 Subject: [PATCH 07/10] soo: fix the header arithmetic of the streamed restore Restoring a snapshot re-implanted the capsule memory four bytes too early, so the capsule resumed on shifted contents and drowned in synchronous exceptions -- or jumped into an unmapped vector and got killed, depending on what the shift landed on. The size AVZ stores at the beginning of a snapshot does not count itself: only the value it hands back to the agency includes those four bytes. Deriving the header size from the stored value therefore misses them, and every chunk was read from the wrong offset. Add the size field back where the header is computed, and read one more byte than the stored size says when handing the header over. AVZ also reads the domain context out of the buffer again at the FINALIZE stage, to restore the EL2 frame of the capsule; the CHUNK stages having filled the buffer with capsule memory by then, put the header back before finalizing. Validated end to end on virt64: two consecutive pause/resume cycles through the EMISO engine, no contiguous allocation involved. --- .../soo/files/soo-generic/0062-soo.h.patch | 9 +++--- .../files/soo-generic/0073-capsule.c.patch | 29 +++++++++++++++++-- so3/so3/soo/include/soo/uapi/soo.h | 7 +++-- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch index 9c74310e1..33ecf062f 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0062-soo.h.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/include/soo/uapi/soo.h 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/include/soo/uapi/soo.h 2025-08-29 08:46:41.395924678 +0200 -@@ -0,0 +1,537 @@ +@@ -0,0 +1,538 @@ +/* + * Copyright (C) 2014-2025 Daniel Rossier + * @@ -400,9 +400,10 @@ +/* AVZ_WRITE_SNAPSHOT */ +/* + * `snapshot_paddr` points to the agency bounce buffer and is read again at each -+ * stage. At the INIT stage the buffer holds the snapshot header (payload size + -+ * domain context); at the CHUNK stage it holds one chunk of capsule memory, at -+ * its very beginning, and `size` tells AVZ how much of it may be used. ++ * stage. At the INIT and FINALIZE stages the buffer holds the snapshot header ++ * (payload size + domain context), which AVZ reads to restore the capsule; at ++ * the CHUNK stage it holds one chunk of capsule memory, at its very beginning, ++ * and `size` tells AVZ how much of it may be used. + */ +typedef struct { + void *snapshot_paddr; diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch index 775e950a9..6419145de 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0073-capsule.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/capsule.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/capsule.c 2025-08-30 15:37:09.704925348 +0200 -@@ -0,0 +1,781 @@ +@@ -0,0 +1,804 @@ +/* + * Copyright (C) 2017-2024 Daniel Rossier + * @@ -439,7 +439,14 @@ + * then reuse for the capsule memory itself. + */ + -+ head_len = (snapshot_size < AVZ_STAGE_CHUNK_SIZE ? snapshot_size : AVZ_STAGE_CHUNK_SIZE); ++ /* The size stored at the beginning of a snapshot does not count itself, ++ * so the buffer holds sizeof(uint32_t) more bytes than it says. ++ */ ++ ++ head_len = snapshot_size + sizeof(snapshot_size); ++ ++ if (head_len > AVZ_STAGE_CHUNK_SIZE) ++ head_len = AVZ_STAGE_CHUNK_SIZE; + + if (copy_from_user(snapshot_bounce, buffer, head_len)) { + printk("%s: Failed to retrieve the snapshot header from user space\n", __func__); @@ -486,7 +493,11 @@ + goto out; + } + -+ header_size = snapshot_size - payload_size; ++ /* The header is the size field plus the domain context, the latter being ++ * whatever the snapshot holds beyond the capsule memory itself. ++ */ ++ ++ header_size = sizeof(snapshot_size) + (snapshot_size - payload_size); + + args.u.avz_snapshot_args.offset = 0; + @@ -524,6 +535,18 @@ + cond_resched(); + } + ++ /* AVZ reads the domain context out of the buffer again at the FINALIZE ++ * stage, to restore the EL2 frame of the capsule: put the header back, ++ * the CHUNK stages having filled the buffer with capsule memory since. ++ */ ++ ++ if (copy_from_user(snapshot_bounce, buffer, head_len)) { ++ printk("%s: Failed to retrieve the snapshot header from user space\n", __func__); ++ ret = -EFAULT; ++ ++ goto out; ++ } ++ + args.u.avz_snapshot_args.stage = AVZ_STAGE_FINALIZE; + + avz_hypercall(&args); diff --git a/so3/so3/soo/include/soo/uapi/soo.h b/so3/so3/soo/include/soo/uapi/soo.h index 7fee5cebc..3e2429a0f 100644 --- a/so3/so3/soo/include/soo/uapi/soo.h +++ b/so3/so3/soo/include/soo/uapi/soo.h @@ -399,9 +399,10 @@ typedef struct { /* AVZ_WRITE_SNAPSHOT */ /* * `snapshot_paddr` points to the agency bounce buffer and is read again at each - * stage. At the INIT stage the buffer holds the snapshot header (payload size + - * domain context); at the CHUNK stage it holds one chunk of capsule memory, at - * its very beginning, and `size` tells AVZ how much of it may be used. + * stage. At the INIT and FINALIZE stages the buffer holds the snapshot header + * (payload size + domain context), which AVZ reads to restore the capsule; at + * the CHUNK stage it holds one chunk of capsule memory, at its very beginning, + * and `size` tells AVZ how much of it may be used. */ typedef struct { void *snapshot_paddr; From c086f3f36470f4ae0b0faa1e3548813a58db2e85 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 15:50:13 +0200 Subject: [PATCH 08/10] build: probe sudo with a real command, not with `sudo -v` sudo_session_start() gated the build on `sudo -v`, which validates the user through the 'validate' pseudo-command. A sudoers file granting NOPASSWD to commands may still refuse that one, and the build then stopped on "failed to acquire sudo credentials" while every privileged task it was about to run needed no authentication at all. Probe with a harmless command instead, and only fall back to `sudo -v` when that fails -- which is where a password prompt genuinely belongs. --- scripts/common/sudo_session.sh | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/common/sudo_session.sh b/scripts/common/sudo_session.sh index 2f5b40416..11f8ada4d 100644 --- a/scripts/common/sudo_session.sh +++ b/scripts/common/sudo_session.sh @@ -17,13 +17,21 @@ # # keep-alive is auto-killed on EXIT/INT/TERM via the installed trap sudo_session_start() { - # Validate the sudo timestamp upfront. With a NOPASSWD sudoers - # entry this is a silent no-op; otherwise the user gets a single + # Validate the sudo timestamp upfront, so the user gets a single # password prompt right here, BEFORE bitbake starts. - if ! sudo -v + # + # The probe is a real (harmless) command rather than `sudo -v`: the + # latter validates the user through the 'validate' pseudo-command, + # which a sudoers granting NOPASSWD to commands may still refuse -- + # and it would then demand an authentication that no privileged task + # of the build actually needs. + if ! sudo -n true 2>/dev/null then - printf "Error: failed to acquire sudo credentials\n" >&2 - return 1 + if ! sudo -v + then + printf "Error: failed to acquire sudo credentials\n" >&2 + return 1 + fi fi # The keep-alive below refreshes the timestamp every 60 s so that From 0a79443d525041f1b2e0b7bd2a06337641cde8e7 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 15:50:50 +0200 Subject: [PATCH 09/10] soo: release what a capsule cannot release itself A capsule closes its frontends and drops its vbstore entries as part of the DC_SHUTDOWN handshake. One which is suspended, stopped or already dead never gets there, and everything it owned outlives it: the backends stay registered, so the next capsule landing in that slot makes vbus_dev_changed() BUG() on a device which already exists, and the vbstore entries stay too, so a capsule restored there comes up on top of what the previous one left. Do it for such a capsule, in shutdown_capsule(), where the handshake is skipped: unregister the backends bound to that domain, and remove its vbstore subtrees -- backend//, device/ and soo/s3c/, mirroring what vbus_probe_backend() walks and what the capsule removes for itself. The bus walk reports one device at a time, holding it, so that device_unregister() runs outside of it. This already applied to a capsule killed after a fault, whose slot could not be reused afterwards. --- .../soo/files/soo-generic/0064-vbus.h.patch | 5 +- .../soo/files/soo-generic/0077-core.c.patch | 17 +++- .../soo-generic/0093-vbus_backend.c.patch | 60 +++++++++++++- .../soo/files/soo-generic/0094-vbus.c.patch | 79 ++++++++++++++++++- 4 files changed, 157 insertions(+), 4 deletions(-) diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0064-vbus.h.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0064-vbus.h.patch index a618647d4..7b85b3276 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0064-vbus.h.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0064-vbus.h.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/include/soo/vbus.h 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/include/soo/vbus.h 2025-08-29 08:46:41.396103436 +0200 -@@ -0,0 +1,336 @@ +@@ -0,0 +1,339 @@ +/* + * Copyright (C) 2014-2019 Daniel Rossier + * Copyright (C) 2016, 2018 Baptiste Delporte @@ -265,10 +265,13 @@ + +extern void vbus_read_otherend_details(struct vbus_device *vdev, char *id_node, char *path_node); +extern int vbus_suspend_dev(struct bus_type *bus, unsigned int domID); ++int vbus_remove_dev(struct bus_type *bus, unsigned int domID); +extern int vbus_resume_dev(struct bus_type *bus, unsigned int domID); + +/* Prepare for domain suspend: then resume or cancel the suspend. */ +int vbus_suspend_devices(unsigned int domID); ++int vbus_remove_devices(unsigned int domID); ++void vbstore_capsule_remove(unsigned int domID); +int vbus_resume_devices(unsigned int domID); + +int vdev_probe(char *node); diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch index 6a1a71546..f14552c42 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0077-core.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/core.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/core.c 2025-08-30 15:29:26.627707308 +0200 -@@ -0,0 +1,353 @@ +@@ -0,0 +1,368 @@ +/* + * Copyright (C) 2014-2019 Daniel Rossier + * Copyright (C) 2017, 2018 Baptiste Delporte @@ -39,6 +39,7 @@ +#include +#include +#include ++#include + +#define AGENCY_DEV_NAME "soo/core" +#define AGENCY_DEV_MAJOR 126 @@ -84,6 +85,20 @@ + do_sync_dom(S3C_slotID, DC_SHUTDOWN); + + S3C_state = get_S3C_state(S3C_slotID); ++ } else { ++ /* That handshake is also what makes the capsule close its frontends, ++ * and a backend is removed when its frontend does so. Nobody will: ++ * tear them down here, or the next capsule to land in this slot finds ++ * them and vbus_dev_changed() refuses to probe over them. ++ */ ++ ++ vbus_remove_devices(S3C_slotID); ++ ++ /* Same for the vbstore entries the capsule would have removed ++ * itself: the next capsule in this slot must not find them. ++ */ ++ ++ vbstore_capsule_remove(S3C_slotID); + } + + /* Then, final termination of the residual S3C */ diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0093-vbus_backend.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0093-vbus_backend.c.patch index 06a77a0f2..073acdb35 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0093-vbus_backend.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0093-vbus_backend.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/vbus/vbus_backend.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/vbus/vbus_backend.c 2025-08-29 08:46:41.395033838 +0200 -@@ -0,0 +1,314 @@ +@@ -0,0 +1,372 @@ +/* + * Copyright (C) 2016-2019 Daniel Rossier + * @@ -295,6 +295,64 @@ +} + +/* ++ * vbus_remove_devices ++ * ++ * Remove the backends of a capsule which cannot close them itself. ++ */ ++int vbus_remove_devices(unsigned int domID) ++{ ++ return vbus_remove_dev(&vbus_backend.bus, domID); ++} ++ ++/* ++ * vbstore_capsule_remove ++ * ++ * Remove the vbstore entries a capsule leaves behind. ++ * ++ * A capsule removes its own entries when it terminates, as part of the ++ * DC_SHUTDOWN handshake. One which is suspended, stopped or already dead never ++ * gets there, so the agency does it in its stead: those entries must not ++ * outlive the capsule which owned them, or the next capsule to land in that ++ * slot comes up on top of what the previous one left. ++ * ++ * The layout mirrors what vbus_probe_backend() walks: one directory per device ++ * type under backend/, each holding a directory per domain. ++ */ ++void vbstore_capsule_remove(unsigned int domID) ++{ ++ char noderoot[VBS_KEY_LENGTH]; ++ char node[VBS_KEY_LENGTH]; ++ char **dir; ++ unsigned int i, dir_n = 0; ++ ++ sprintf(node, "%d", domID); ++ ++ DBG("Removing the vbstore entries of domain %d...\n", domID); ++ ++ dir = vbus_directory(VBT_NIL, "backend", "", &dir_n); ++ if (IS_ERR(dir)) ++ return; ++ ++ for (i = 0; i < dir_n; i++) { ++ strcpy(noderoot, "backend/"); ++ strcat(noderoot, dir[i]); ++ ++ if (vbus_directory_exists(VBT_NIL, noderoot, node)) ++ vbus_rm(VBT_NIL, noderoot, node); ++ } ++ ++ kfree(dir); ++ ++ /* The frontend side, and the capsule entry itself. */ ++ ++ if (vbus_directory_exists(VBT_NIL, "device", node)) ++ vbus_rm(VBT_NIL, "device", node); ++ ++ if (vbus_directory_exists(VBT_NIL, "soo/s3c", node)) ++ vbus_rm(VBT_NIL, "soo/s3c", node); ++} ++ ++/* + * vbus_resume_devices + * + * Inform all vbus devices that we are preparing to suspend (or some other scenarios requiring diff --git a/build/meta-linux/recipes-linux/soo/files/soo-generic/0094-vbus.c.patch b/build/meta-linux/recipes-linux/soo/files/soo-generic/0094-vbus.c.patch index c91c6abe1..979cb6232 100644 --- a/build/meta-linux/recipes-linux/soo/files/soo-generic/0094-vbus.c.patch +++ b/build/meta-linux/recipes-linux/soo/files/soo-generic/0094-vbus.c.patch @@ -1,6 +1,6 @@ --- /home/rossierd/soo/git/micofe/build/tmp/work/linux-6.12-r0/linux-6.12/soo/kernel/vbus/vbus.c 1970-01-01 01:00:00.000000000 +0100 +++ ./soo/kernel/vbus/vbus.c 2025-08-29 08:46:41.395052929 +0200 -@@ -0,0 +1,870 @@ +@@ -0,0 +1,947 @@ + +/* + * Copyright (C) 2014-2019 Daniel Rossier @@ -695,6 +695,83 @@ + return 0; +} + ++/* ++ * Look for a backend still attached to a capsule. The device is reported back ++ * instead of being removed on the spot: device_unregister() must not run from ++ * within the bus walk. ++ */ ++struct vb_domain_info { ++ unsigned int domID; ++ struct vbus_device *vdev; ++}; ++ ++static int find_dev_of_domain(struct device *dev, void *data) ++{ ++ struct vb_domain_info *info = (struct vb_domain_info *) data; ++ struct vbus_device *vdev = (struct vbus_device *) to_vbus_device(dev); ++ unsigned int curDomID; ++ char *ptr_item; ++ char item[80]; ++ ++ /* Node names are shaped as backend///. */ ++ ++ ptr_item = strchr(vdev->nodename, '/'); ++ if (ptr_item == NULL) ++ return 0; ++ ++ ptr_item = strchr(ptr_item + 1, '/'); ++ if (ptr_item == NULL) ++ return 0; ++ ++ if (sscanf(ptr_item + 1, "%d/%s", &curDomID, item) < 1) ++ return 0; ++ ++ if (curDomID != info->domID) ++ return 0; ++ ++ info->vdev = vdev; ++ ++ /* Hold the device until the caller is done removing it, the walk being ++ * over by then. ++ */ ++ ++ get_device(dev); ++ ++ return 1; ++} ++ ++/* ++ * Remove every backend a capsule leaves behind. ++ * ++ * A backend is normally removed when its frontend switches to Closing, which ++ * the DC_SHUTDOWN handshake drives. A capsule which is suspended, stopped or ++ * already dead cannot answer that handshake, so the agency has to clean up on ++ * its own before the slot is handed over: the next capsule landing there would ++ * otherwise find those nodes, and vbus_dev_changed() refuses to probe a device ++ * which already exists. ++ */ ++int vbus_remove_dev(struct bus_type *bus, unsigned int domID) ++{ ++ struct vb_domain_info info; ++ ++ while (true) { ++ info.domID = domID; ++ info.vdev = NULL; ++ ++ bus_for_each_dev(bus, NULL, &info, find_dev_of_domain); ++ ++ if (info.vdev == NULL) ++ break; ++ ++ DBG("Removing the leftover backend %s\n", info.vdev->nodename); ++ ++ device_unregister(&info.vdev->dev); ++ put_device(&info.vdev->dev); ++ } ++ ++ return 0; ++} ++ +/******************/ + +/* From fc6c9e81d817d88e9d56011a9c25daf869238e98 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Mon, 7 Sep 2026 17:12:40 +0200 Subject: [PATCH 10/10] avz: free the grant table of a destroyed domain complete_domain_destroy() released the shared page, the stack and the domain structure, but never walked d->gnttab. A domain revokes its grants one by one as its frontends close, so the list is empty by then and the leak never showed -- except for a domain destroyed before it gets there: suspended for a snapshot, crashed, or killed outright. Its entries then outlive the domain they describe, and the memory is handed out again to the next one, which inherits list links pointing into it. Add gnttab_destroy(), symmetric with gnttab_init(), and call it where the rest of the domain is released. --- so3/so3/avz/include/avz/gnttab.h | 1 + so3/so3/avz/kernel/domain.c | 6 ++++++ so3/so3/avz/kernel/gnttab.c | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/so3/so3/avz/include/avz/gnttab.h b/so3/so3/avz/include/avz/gnttab.h index dbfa64d49..b0ccf5e3d 100644 --- a/so3/so3/avz/include/avz/gnttab.h +++ b/so3/so3/avz/include/avz/gnttab.h @@ -38,6 +38,7 @@ struct gnttab { typedef struct gnttab gnttab_t; void gnttab_init(struct domain *d); +void gnttab_destroy(struct domain *d); void do_gnttab(gnttab_op_t *args); addr_t map_vbstore_pfn(int target_domid, int pfn); diff --git a/so3/so3/avz/kernel/domain.c b/so3/so3/avz/kernel/domain.c index 379a07bef..37137e9cc 100644 --- a/so3/so3/avz/kernel/domain.c +++ b/so3/so3/avz/kernel/domain.c @@ -134,6 +134,12 @@ static void complete_domain_destroy(struct domain *d) { sched_destroy_domain(d); +#ifdef CONFIG_SOO + /* The grants this domain still holds die with it. */ + + gnttab_destroy(d); +#endif /* CONFIG_SOO */ + /* Remove the root page table */ reset_root_pgtable((void *) d->pagetable_l0_vaddr, true); diff --git a/so3/so3/avz/kernel/gnttab.c b/so3/so3/avz/kernel/gnttab.c index e2df5b23d..e9cb2346e 100644 --- a/so3/so3/avz/kernel/gnttab.c +++ b/so3/so3/avz/kernel/gnttab.c @@ -105,6 +105,25 @@ gnttab_t *new_gnttab_entry(struct domain *d, domid_t target_domid, addr_t pfn) return gnttab; } +/** + * @brief Release the whole grant table of a domain. + * + * A domain revokes its grants one by one, as its frontends close. One which is + * destroyed before it gets there -- suspended for a snapshot, crashed, or + * killed outright -- leaves them all behind, and the entries would then outlive + * the domain they describe: the memory is recycled by the next domain, which + * inherits list links pointing into it. + */ +void gnttab_destroy(struct domain *d) +{ + gnttab_t *cur, *tmp; + + list_for_each_entry_safe(cur, tmp, &d->gnttab, list) { + list_del(&cur->list); + free(cur); + } +} + void revoke_gnttab_entry(struct domain *d, grant_ref_t ref) { gnttab_t *cur;