Skip to content

Conversation

@GGuche-2XS-Univ-Lille
Copy link
Contributor

Contribution description

The xipfs filesystem is able to perform in-place execution.

The aim of this PR is to add hardware MPU-based memory isolation to the xipfs' execution capability.

With it, an executable fae file is run in non-privileged mode and can only access to its legitimate memory areas, thanks to MPU regions definitions of TEXT, DATA and STACK segments.
TEXT is read-only, but DATA and STACK are read-write.

This generic hardware approach can handle all executables, with almost zero time overhead.
MPU regions identifiers have been chosen with respect to RIOT's MPU safe guard and MPU no exec functionalities.

This feature should be available for all platforms with an ARM cpu and an ARMv7-M MPU, but has been only tested on QORVO DWM1001 for now.

Testing procedure

For this board, a memory dumper is provided in xipfs' example to illustrate simple/safe memory dumps of legit/non-legit ram/rom.

> drop_files
2025-10-01 14:22:34,102 # drop_files
> ls /nvme0p0
2025-10-01 14:22:37,829 # ls /nvme0p0
2025-10-01 14:22:37,831 # hello-world.fae       1376 B
2025-10-01 14:22:37,833 # dumper.fae    1920 B
2025-10-01 14:22:37,834 # total 2 files
> execute /nvme0p0/dumper.fae legit-ram
2025-10-01 14:22:38,303 # execute /nvme0p0/dumper.fae legit-ram
2025-10-01 14:22:38,314 # 20004050  00 00 00 00 00 00 00 00   b0 04 00 20 d4 04 00 20  |........... ... |
2025-10-01 14:22:38,321 # 20004060  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  |................|
2025-10-01 14:22:38,328 # 20004070  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  |................|
2025-10-01 14:22:38,335 # 20004080  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  |................|
2025-10-01 14:22:38,336 # 20004090
> execute -p /nvme0p0/dumper.fae legit-ram
2025-10-01 14:23:09,935 # execute -p /nvme0p0/dumper.fae legit-ram
2025-10-01 14:23:09,946 # 20004050  01 00 00 00 fc 77 00 20   00 00 00 00 00 00 00 00  |.....w. ........|
2025-10-01 14:23:09,954 # 20004060  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  |................|
2025-10-01 14:23:09,961 # 20004070  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  |................|
2025-10-01 14:23:09,968 # 20004080  00 00 00 00 00 00 00 00   00 00 00 00 00 00 00 00  |................|
2025-10-01 14:23:09,969 # 20004090
> execute /nvme0p0/dumper.fae non-legit-ram
2025-10-01 14:23:26,575 # execute /nvme0p0/dumper.fae non-legit-ram
2025-10-01 14:23:26,586 # 20000020  fe e7 fe e7 fe e7 fe e7   fe e7 fe e7 fe e7 fe e7  |................|
2025-10-01 14:23:26,594 # 20000030  fe e7 fe e7 fe e7 fe e7   fe e7 fe e7 fe e7 fe e7  |................|
2025-10-01 14:23:26,601 # 20000040  fe e7 fe e7 fe e7 fe e7   fe e7 fe e7 fe e7 fe e7  |................|
2025-10-01 14:23:26,608 # 20000050  fe e7 fe e7 fe e7 fe e7   fe e7 fe e7 fe e7 fe e7  |................|
2025-10-01 14:23:26,609 # 20000060
> execute -p /nvme0p0/dumper.fae non-legit-ram
2025-10-01 14:23:47,488 # execute -p /nvme0p0/dumper.fae non-legit-ram
2025-10-01 14:23:47,496 # 20000020  Illegal memory access detected at 0x20000020.
2025-10-01 14:23:47,501 # Failed to execute '/nvme0p0/dumper.fae', error=-5 (I/O error)
> execute /nvme0p0/dumper.fae legit-rom
2025-10-01 14:24:52,687 # execute /nvme0p0/dumper.fae legit-rom
2025-10-01 14:24:52,698 # 1c470  2d e9 f8 43 04 46 00 f1   40 07 bc 42 a0 46 07 d3  |-..C.F..@..B.F..|
2025-10-01 14:24:52,705 # 1c480  2d 4b 21 46 5a f8 03 00   bd e8 f8 43 00 f0 c8 b8  |-K!FZ......C....|
2025-10-01 14:24:52,712 # 1c490  2a 4b 21 46 5a f8 03 00   00 f0 c2 f8 28 4b 5a f8  |*K!FZ.......(KZ.|
2025-10-01 14:24:52,719 # 1c4a0  03 90 26 46 00 25 16 f8   01 1b 01 35 48 46 00 f0  |..&F.%.....5HF..|
2025-10-01 14:24:52,719 # 1c4b0
> execute -p /nvme0p0/dumper.fae legit-rom
2025-10-01 14:25:16,344 # execute -p /nvme0p0/dumper.fae legit-rom
2025-10-01 14:25:16,355 # 1c470  2d e9 f8 43 04 46 00 f1   40 07 bc 42 a0 46 07 d3  |-..C.F..@..B.F..|
2025-10-01 14:25:16,362 # 1c480  2d 4b 21 46 5a f8 03 00   bd e8 f8 43 00 f0 c8 b8  |-K!FZ......C....|
2025-10-01 14:25:16,369 # 1c490  2a 4b 21 46 5a f8 03 00   00 f0 c2 f8 28 4b 5a f8  |*K!FZ.......(KZ.|
2025-10-01 14:25:16,376 # 1c4a0  03 90 26 46 00 25 16 f8   01 1b 01 35 48 46 00 f0  |..&F.%.....5HF..|
2025-10-01 14:25:16,377 # 1c4b0
> execute /nvme0p0/dumper.fae non-legit-rom
2025-10-01 14:25:49,232 # execute /nvme0p0/dumper.fae non-legit-rom
2025-10-01 14:25:49,242 # 00  00 04 00 20 3d 0a 00 00   c5 09 00 00 81 09 00 00  |... =...........|
2025-10-01 14:25:49,249 # 10  15 0a 00 00 d5 09 00 00   e5 09 00 00 00 00 00 00  |................|
2025-10-01 14:25:49,256 # 20  00 00 00 00 00 00 00 00   00 00 00 00 31 09 00 00  |............1...|
2025-10-01 14:25:49,262 # 30  f5 09 00 00 00 00 00 00   e5 08 00 00 05 0a 00 00  |................|
2025-10-01 14:25:49,263 # 40
> execute -p /nvme0p0/dumper.fae non-legit-rom
2025-10-01 14:26:23,457 # execute -p /nvme0p0/dumper.fae non-legit-rom
2025-10-01 14:26:23,464 # 0  Illegal memory access detected at 0x0.
2025-10-01 14:26:23,468 # Failed to execute '/nvme0p0/dumper.fae', error=-5 (I/O error)

On a sidenote, illegitimate RAM address has been chosen to be equal to 0x20000020, because the first 32 bytes of the RAM are in read-only MPU stack guard region.
In safe execution mode, the dumper would display the 32 bytes starting at 0x20000000 before the MPU detects an illegal access, which could lead to deceptive conclusions.

@github-actions github-actions bot added Platform: ARM Platform: This PR/issue effects ARM-based platforms Area: doc Area: Documentation Area: pkg Area: External package ports Area: cpu Area: CPU/MCU ports Area: sys Area: System Area: examples Area: Example Applications labels Oct 2, 2025
@crasbe crasbe added Type: enhancement The issue suggests enhanceable parts / The PR enhances parts of the codebase / documentation CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR labels Oct 3, 2025
@riot-ci
Copy link

riot-ci commented Oct 3, 2025

Murdock results

FAILED

5b86137 fixup! pkg/xipfs: add MPU memory isolation to file execution

Success Failures Total Runtime
3700 0 9883 03m:34s

Artifacts

@Teufelchen1
Copy link
Contributor

Where can I look at the code of examples/advanced/xipfs/dumper.fae?

@GGuche-2XS-Univ-Lille
Copy link
Contributor Author

@Teufelchen1 Sorry for the delay. You can find the source of the memory dumper here.

It comes with the tools for building : the sources are compiled to an elf file, which is processed in order to build a fae file.
Fae files are lighter than elf ones and suited to our needs (own crt0 performing relocation etc...).

For more details on the tools and process, please go to the XIPFS Format repository

Copy link
Contributor

@crasbe crasbe left a comment

Choose a reason for hiding this comment

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

I don't really like the modifications to the common Cortex-M files to be honest.
This is pretty application specific and does not really belong here.

I acknowledge that currently there are no provisions to change the mem_manage handler or add cases to the SVC dispatcher, but I'm afraid that this is not a good way yet.

@GGuche-2XS-Univ-Lille
Copy link
Contributor Author

GGuche-2XS-Univ-Lille commented Oct 6, 2025

@crasbe XIPFS is designed for ARM platforms with addressable flash memory and ARMv7-M MPU.

As such, we don't target any board specifically but more a family of boards sharing these features.
Plug in the code in the common part of cortex-m cpu appeared to us as the appropriate move.

Furthermore, we need to customize the memory fault handler :

  • to dynamically set TEXT mpu region, when PC jumps over flash pages,
  • to stop the execution when an illegal memory access is detected.

We also need to add svc cases :

  • to leave the thread privileged mode and enter the handler one, which leads finally to enter the user mode,
  • to handle an API of syscalls (quite small for now, but that will grow in the future), and keep the fae files footprint low.

We would have liked to come with a better proposal, satisfying all of us, but to the best of our knowledge, we need at least these two modifications to make the MPU memory isolation work for the targetted platforms.

@GGuche-2XS-Univ-Lille
Copy link
Contributor Author

GGuche-2XS-Univ-Lille commented Oct 8, 2025

@crasbe To be less invasive, would you agree on using callbacks in _svc_dispatch and mem_manage_default ?

For example, __svc_dispatch in thread_arch.c :

typedef void (*svc_dispatch_handler_t)(unsigned int svc_number, unsigned int *svc_args);

svc_dispatch_handler_t _svc_dispatch_handler = NULL;

...

    switch (svc_number) {
        case 1: /* SVC number used by cpu_switch_context_exit */
            SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
            break;
        default:
            if (_svc_dispatch_handler != NULL) {
                _svc_dispatch_handler(svc_number, svc_args);
            } else {
                DEBUG("svc: unhandled SVC #%u\n", svc_number);
            }
            break;
    }
...

And for mem_manage_default in vector_cortexm.c,

typedef int (*mem_manage_handler_t)(void);

mem_manage_handler_t mem_manage_handler = NULL;

...

    if (mem_manage_handler != NULL) {
        if (mem_manage_handler() == 1) {
            return;
        }
    }
    core_panic();

The callbacks would be be set to valid handlers before calling xipfs_safe_execv.
After it, they will be removed -set to NULL-.

Is it more compatible with what you have in mind ?

@crasbe
Copy link
Contributor

crasbe commented Oct 8, 2025

Yes, this looks good. I had something similar in mind, but I wasn't able to articulate a proper proposal. 👍

@crasbe
Copy link
Contributor

crasbe commented Oct 8, 2025

Perhaps for the SVC we could also modify it like this:

typedef void (*svc_dispatch_handler_t)(unsigned int svc_number, unsigned int *svc_args);

svc_dispatch_handler_t _svc_dispatch_handler = NULL;

...

    switch (svc_number) {
        case 1: /* SVC number used by cpu_switch_context_exit */
            SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
            break;
        default:
            /* call user defined SVC dispatch handler */
            if (_svc_dispatch_handler != NULL) {
                if (_svc_dispatch_handler(svc_number, svc_args) > 0) {
                    break;
                }
            }
            DEBUG("svc: unhandled SVC #%u\n", svc_number);
            break;
    }
...

And _svc_dispatch_handler could return -ENOTSUP if the SVC number that it was called with is not handled.

@GGuche-2XS-Univ-Lille
Copy link
Contributor Author

@crasbe Did the modifications.

There are some doccheck failing in static tests, but regarding to code not modified by the PR.
Should I fix them or let them as is ?

Copy link
Contributor

@crasbe crasbe left a comment

Choose a reason for hiding this comment

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

Some style related comments.

Although I do wonder if it wouldn't make sense to break the SVC and Mem Handler part out into a separate PR.

* @retval < 0 on NULL handler or if a handler has already been set
* @retval >= 0 otherwise
*/
int set_svc_dispatch_handler(svc_dispatch_handler_t handler);
Copy link
Member

Choose a reason for hiding this comment

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

Just to be sure I'm not overlooking something:

Why does this need to be set dynamically at run time, when there is only a single callback slot for it anyway?

Why not just provide a weak no-op default handler and register at link time by providing a handler with a strong symbol?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@maribu That's an interesting question.

In the context of xipfs, we are mostly dealing with short lifecycle execution.
The rationale for the dynamic setting is to scale among usages; other software could use this mechanism without collisions.

Now the weak function is attractive, because of its simplicity, but only one actor could use it, here xipfs.

While we are at it, do you prefer to handle this modification in a separate PR too ?


#endif

int set_memory_manage_handler(mem_manage_handler_t handler) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to set this at run-time or would it be sufficient if this was a weak symbol that gets replaced at link-time?

Copy link
Member

Choose a reason for hiding this comment

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

Hah, I was faster! Quick-draw batch for me 🤣

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@crasbe @maribu @benpicco
To sum it up :

  • thread_arch.c
__attribute__((weak)) int svc_dispatch_handler(unsigned int svc_number, unsigned int *svc_args) {
  return -ENOTSUP;
}
...
   switch (svc_number) {
        case 1: /* SVC number used by cpu_switch_context_exit */
            SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
            break;
        default:
             if (_svc_dispatch_handler(svc_number, svc_args) >= 0) {
                 return;
             }
            DEBUG("svc: unhandled SVC #%u\n", svc_number);
            break;
    }
  • vector_cortexm.c:
__attribute__((weak)) int mem_manage_handler(void) {
  return 0;
}

void mem_manage_default(void)
{
    if (_mem_manage_handler() == 1) {
         return;
    }
    core_panic(PANIC_MEM_MANAGE, "MEM MANAGE HANDLER");
}

The all above in a separate PR to keep things clean ?
What would be the PR title then ?

Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't insist for separating that out, this PR is not too large anyway. IMO: Whatever works best for you.

@GGuche-2XS-Univ-Lille
Copy link
Contributor Author

@crasbe @maribu @benpicco Here is the implementation with the weak functions.

Copy link
Contributor

@crasbe crasbe left a comment

Choose a reason for hiding this comment

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

I like this solution a lot more, thanks for changing it!

Hopefully I'll find the time for testing it soon.

Comment on lines +67 to +73
#ifdef XIPFS_ENABLE_SAFE_EXEC_SUPPORT
int ret = (exe_filename_arg_pos == 2) ?
xipfs_extended_driver_safe_execv(exe_filename, execute_file_handler_args)
: xipfs_extended_driver_execv(exe_filename, execute_file_handler_args);
#else
int ret = xipfs_extended_driver_execv(exe_filename, execute_file_handler_args);
#endif
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
#ifdef XIPFS_ENABLE_SAFE_EXEC_SUPPORT
int ret = (exe_filename_arg_pos == 2) ?
xipfs_extended_driver_safe_execv(exe_filename, execute_file_handler_args)
: xipfs_extended_driver_execv(exe_filename, execute_file_handler_args);
#else
int ret = xipfs_extended_driver_execv(exe_filename, execute_file_handler_args);
#endif
if (IS_DEFINED(XIPFS_ENABLE_SAFE_EXEC_SUPPORT) && exe_filename_arg_pos == 2) {
int ret = xipfs_extended_driver_safe_execv(exe_filename, execute_file_handler_args);
}
else {
int ret = xipfs_extended_driver_execv(exe_filename, execute_file_handler_args);
}

Perhaps a little easier to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I may be wrong but I don't think this would compile successfully.
retwould be out of scope for usage later on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why should it be out of scope? Both code paths have int ret.

Copy link
Contributor Author

@GGuche-2XS-Univ-Lille GGuche-2XS-Univ-Lille Oct 31, 2025

Choose a reason for hiding this comment

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

Yes, they both have ìnt ret, but past line 69 or line 72, ret would be out of scope of the selected if branch.
I tried to compile the following snippet :

#include <stdlib.h>

int main(int argc, const char *argv[]) {

    if (argc > 1) {
        int ret = argc;
    } else {
        int ret = -argc;
    }

    return ret;
}

And I got :

gcc -std=c99 -Wall -Werror -o test test.c
test.c: In function ‘main’:
test.c:7:13: error: unused variable ‘ret’ [-Werror=unused-variable]
    7 |         int ret = argc;
      |             ^~~
test.c:9:13: error: unused variable ‘ret’ [-Werror=unused-variable]
    9 |         int ret = -argc;
      |             ^~~
test.c:12:12: error: ‘ret’ undeclared (first use in this function)
   12 |     return ret;
      |            ^~~
test.c:12:12: note: each undeclared identifier is reported only once for each function it appears in
cc1: all warnings being treated as errors

Am I missing something ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: cpu Area: CPU/MCU ports Area: doc Area: Documentation Area: examples Area: Example Applications Area: pkg Area: External package ports Area: sys Area: System CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR Platform: ARM Platform: This PR/issue effects ARM-based platforms Type: enhancement The issue suggests enhanceable parts / The PR enhances parts of the codebase / documentation

6 participants