Bug 15244 - pseudo: support for io_uring syscalls
Summary: pseudo: support for io_uring syscalls
Status: NEW
Alias: None
Product: Pseudo
Classification: Yocto Project Subprojects
Component: pseudo (show other bugs)
Version: 5.99
Hardware: x86 Multiple
: Medium+ normal
Target Milestone: 6.99
Assignee: Unassigned
QA Contact:
URL:
Whiteboard:
Depends on:
Blocks:
 
Reported: 2023-10-19 11:49 UTC by Martin Jansa
Modified: 2026-10-19 09:35 UTC (History)
9 users (show)

See Also:
OS type for building Yocto: ---
Type of Regression: ---
Verified:
Documentation change: Don't know


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Martin Jansa 2023-10-19 11:49:51 UTC
It was first discovered in nodejs-native as reported in:
https://lists.openembedded.org/g/openembedded-devel/message/105583

with simpler reproducer and a work around provided in:
https://github.com/shr-project/com.webos.app.minimal/commit/bd238047c8ce3cd085041d276613396b863213cf

For long-term solution we should add some support for io_uring in pseudo as more tools will soon use it.

I've created much simpler test case in meta-selftest which just writes to file with writev(), but it doesn't reproduce the issue yet, because I haven't figured out how to pass fd of output file without using open() which is correctly intercepted by pseudo.

Current WIP reproducer is in:
https://git.openembedded.org/openembedded-core-contrib/log/?h=jansa/io_uring

Will update this ticket once this simple reproducer works and triggers the issue.
Comment 1 Martin Jansa 2023-10-19 12:56:48 UTC
There is also cp with liburing in: https://github.com/shuveb/io_uring-by-example/blob/master/04_cp_liburing/main.c

But that also results with openat() for the output file:

io_uring-by-example $ strace -v ./cp_liburing  LICENSE LICENSE2 2>&1 | grep openat.*LICENSE2
openat(AT_FDCWD, "LICENSE2", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 4
Comment 2 Randy MacLeod 2023-10-19 14:45:46 UTC
Does the code in nodejs have fallbacks? If so we can return ENOSYS.
Comment 3 Randy MacLeod 2023-12-21 23:28:32 UTC
Bulk move of 52 5.0-M1 bugs to M2.
Comment 4 Randy MacLeod 2024-01-25 14:28:48 UTC
Bulk move of bugs owned by "unassigned" from 5.0-M2 to 5.0-M3.
Comment 5 Randy MacLeod 2024-02-22 18:37:56 UTC
Martin, Is this a must-have for 5.0 - M4 ?
Comment 6 Martin Jansa 2024-02-22 18:55:38 UTC
It's not must-have, at least the nodejs cases was worked around by reverting that libuv change in:

https://git.openembedded.org/meta-openembedded/commit/?id=d08453978c31ee41d28206c6ff198d7d9d701d88

so we're OK until something else starts using it.
Comment 7 Changqing Li 2024-04-11 08:08:37 UTC
Since the io_uring operation using the syscalls directly we might not be able to intercept in pseudo.

Randy's suggestion about return "ENOSYS" can fix this single issue since libuv has  fallback, if io_uring_setup return -1,  it will use original epoll. But maybe this is not good change for pseudo. libpseudo is a common lib, it need to cover all the cases. if there is another binary use io_uring,  and don't have fallback,  it will always failed, this is not a right way.

Maybe current status for pseudo is better,  if io_uring cause error like Exception: Exception: KeyError: 'getpwuid(): uid not found: 20561',  user
can fix chown root:root -R ${D}" in do_install.
Comment 8 Dmitry Sakhonchik 2026-08-31 08:46:58 UTC
I've updated Martin's test case so it gets fd through io_uring as well.

https://patchwork.yoctoproject.org/project/oe-core/patch/20260831080223.124050-1-frezidok1@gmail.com/

I've used IORING_OP_OPENAT, so now no openat() syscall actually happens and created file completely bypasses pseudo (no trace about this file in pseudo db anymore). 

frezidok@ubuntu1:~$ strace -v ./io-uring-writev test 2>&1 | grep openat
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

bitbake io-uring-writev also successfully fails (sounds odd, I know ;D):

Exception: Exception: KeyError: 'getpwuid(): uid not found: 1000'
Path /mnt/ssd/yocto/opensource-work/bitbake-builds/poky-wrynose/build/tmp/work/cortexa53-crypto-poky-linux/io-uring-writev/1.0/sstate-build-package/package/test is owned by uid 1000, gid 1000, which doesn't match any user/group on target. This may be due to host contamination.

P.S. Big thanks to Marin for the rich comments about io_uring in the code, it helped a lot.
Comment 9 Martin Jansa 2026-08-31 10:23:27 UTC
Thank you for figuring out what I was missing. I've applied it in my contrib branch and confirmed that I can trigger the issue on my system.
Comment 10 Dmitry Sakhonchik 2026-08-31 14:29:08 UTC
As for the problem itself, if we want pseudo to fully support io_uring, I think we need a completely different approach.

The main issue is that pseudo can't intercept syscalls directly. I think we could try to use seccomp for this.

The rough design would be:

1. A pseudo client installs a seccomp filter using SECCOMP_FILTER_FLAG_NEW_LISTENER, returning SECCOMP_RET_USER_NOTIF only for the io_uring syscalls that pseudo needs to observe (io_uring_setup, io_uring_enter, etc).

2. The listener fd is passed to the pseudo server, so it becomes the supervisor.

3. The supervised (target) process tries to call io_uring_setup. Pseudo intercepts it through seccomp, performs the setup itself, and then duplicates the ring fd into the target process using SECCOMP_IOCTL_NOTIF_ADDFD, while copying the params with something like process_vm_writev().

After that, both pseudo and the target process have an fd referring to the same ring, so pseudo can mmap the SQ ring, CQ ring, and SQE array into its own address space and access them at any time.

For example, when the target process tries to call io_uring_enter, pseudo intercepts it, inspects the SQE, maybe substitutes something, then sends "continue" to the target, which proceeds as if nothing happened.

But I couldn't figure out what to do with the CQ. The kernel's response will be placed in shared memory, so the target can read it at any time without making any syscalls, and pseudo may not process it first. So there is a race between pseudo and the target.

If the target uses something like:

int ret = io_uring_enter(s->ring_fd, 1, 1,
                         IORING_ENTER_GETEVENTS);


pseudo could potentially perform this call itself, wait for the kernel to return the result, modify it, and only then let the target continue.

But if the operation is fully asynchronous "submit only and don't wait for completion" I don't currently see a way to guarantee that pseudo can process the CQE before the target reads it.

If someone have any thoughts, I have time to work on this and and work out couple ideas.
Comment 11 David Nyström 2026-10-19 09:34:58 UTC
I took a stab at this.

docker default profile, docker sbx and docker gvisor already blocks io_uring with an EPERM/ENOSYS failure, so ability to do a build without io_uring is likely wanted.
Getting the same behaviour everywhere might be a good idea.

Due to no_new_privs now set in the default task on master branch, that would allow us to use unpriv seccomp to deny, which would be required since popular libraries such as liburing uses direct syscalls for performance.
But there are exceptions for tasks with the "network" flag applied, like do_devshell, do_package, where both don't have no_new_privs, since any emulation would be a stubbed interface, where most of the asynchronicity is synchronously emulated.
I have not found a way to both intercept the ring content and also allow async operations, so any emulation layer will be functionally incomplete, which is arguably worse than rejecting IMO.

For partitial emulation SECCOMP_RET_TRAP was usable, since it defeats RET_ERRNO in the default docker seccomp rule, it works for the io_uring APIs I can synchronously emulate, but with severe limitations to SQPOLL and other async functions that need to be visible to the end application. 
The end result for partitial emulation was rather unpalatable, and race prone.

We could default deny via seccomp syscall interception (where no_new_privs are available) to leverage application fallbacks. 
Upstreaming io_uring fallbacks seems doable, and would likely be supported also by other distros, since the problem is not unique to our fakeroot solution, or docker builds.
Also seems simpler than maintaining our own partitial emulation layer. 

Supporting the complete io_uring API more or less requires migration away from pseudo, and into something completely kernel backed, like a fusermount:ing solution, which maps poorly to the current taskwise pseudo usage.

Sharing a github link to an AI-gen PoC which will "seccomp deny by default deny io_uring iff NNP, else passthough io_uring", where partitial emulation PoC part is intentionally omitted.
https://github.com/nysan/pseudo/commit/a6f57aaf711d546f5f9898a0a0eae5e9f97e2776

Let me know if you agree to the "deny" approach. Perhaps the simplest, and most maintainable approach is to require kernel.io_uring_disabled=1 as a host build prereq.