LATX, fix: lazily start the RCU callback thread

Starting the RCU callback consumer during process initialization exposes an
extra host thread before the guest creates one. Namespace operations that
require a single-threaded caller can then fail for a single-threaded guest.

Start the consumer when callback work first needs it, while preserving the
existing deferred start required by namespace children. Add unit and
integration coverage for the lazy lifecycle and host-thread visibility.

Signed-off-by: yuerengan <y347812075@163.com>
This commit is contained in:
yuerengan 2026-07-31 20:41:44 +08:00 committed by Hanlu Li
parent 7fc3fb1366
commit c90d0b6901
6 changed files with 179 additions and 5 deletions

View File

@ -33,4 +33,15 @@ if host_machine.cpu_family() == 'loongarch64' and \
suite: 'latx-integration',
timeout: 30,
)
test(
'test-rcu-thread-visibility',
find_program('test-rcu-thread-visibility.sh'),
args: [
emulators['latx-x86_64'],
files('rcu-thread-visibility.S'),
],
protocol: 'exitcode',
suite: 'latx-integration',
timeout: 30,
)
endif

View File

@ -0,0 +1,57 @@
.equ __NR_getpid, 39
.equ __NR_setpgid, 109
.equ __NR_getpgid, 121
.equ __NR_exit, 60
.equ SCAN_LIMIT, 128
.section .text
.global _start
.type _start, @function
_start:
mov $__NR_setpgid, %eax
xor %edi, %edi
xor %esi, %esi
syscall
test %rax, %rax
js setup_failed
mov $__NR_getpid, %eax
syscall
mov %eax, %r12d
mov $__NR_getpgid, %eax
xor %edi, %edi
syscall
test %rax, %rax
js setup_failed
mov %eax, %r13d
lea 1(%r12), %r14d
lea SCAN_LIMIT(%r14), %r15d
scan_next:
mov $__NR_getpgid, %eax
mov %r14d, %edi
syscall
cmp %r13d, %eax
je internal_thread_visible
inc %r14d
cmp %r15d, %r14d
jl scan_next
xor %edi, %edi
jmp exit
setup_failed:
mov $11, %edi
jmp exit
internal_thread_visible:
mov $10, %edi
exit:
mov $__NR_exit, %eax
syscall
.size _start, .-_start
.section .note.GNU-stack,"",@progbits

View File

@ -0,0 +1,41 @@
#!/bin/sh
set -eu
emulator=$1
source_file=$2
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT HUP INT TERM
if command -v clang-19 >/dev/null 2>&1; then
clang=clang-19
elif command -v clang >/dev/null 2>&1; then
clang=clang
else
echo "SKIP: clang is required to build the x86_64 guest"
exit 77
fi
"$clang" --target=x86_64-linux-gnu -fuse-ld=lld -nostdlib -static \
-Wl,--build-id=none "$source_file" -o "$workdir/rcu-thread-visibility"
set +e
LATX_AOT=0 LATX_KZT=0 "$emulator" "$workdir/rcu-thread-visibility"
ret=$?
set -e
case $ret in
0)
echo "PASS: no internal LATX thread is visible through getpgid"
;;
10)
echo "FAIL: an internal LATX thread is visible through getpgid" >&2
;;
11)
echo "FAIL: process-group setup failed" >&2
;;
*)
echo "FAIL: unexpected guest exit status $ret" >&2
;;
esac
exit "$ret"

View File

@ -39,3 +39,16 @@ if 'CONFIG_LATX' in config_host
suite: 'lat-pr-fast',
)
endif
test_rcu_lazy_thread = executable(
'test-rcu-lazy-thread',
files('test-rcu-lazy-thread.c'),
include_directories: include_directories('../..'),
dependencies: [glib, qemuutil],
)
test(
'test-rcu-lazy-thread',
test_rcu_lazy_thread,
suite: 'lat-pr-fast',
)

View File

@ -0,0 +1,33 @@
#include "qemu/osdep.h"
#include "qemu/rcu.h"
#include "qemu/thread.h"
typedef struct TestRcuCallback {
struct rcu_head rcu;
QemuEvent done;
} TestRcuCallback;
static void test_callback(struct rcu_head *head)
{
TestRcuCallback *callback = container_of(head, TestRcuCallback, rcu);
qemu_event_set(&callback->done);
}
int main(void)
{
TestRcuCallback callback;
#ifdef CONFIG_LATX
g_assert_false(rcu_call_thread_is_running());
#endif
qemu_event_init(&callback.done, false);
call_rcu1(&callback.rcu, test_callback);
qemu_event_wait(&callback.done);
g_assert_true(rcu_call_thread_is_running());
qemu_event_destroy(&callback.done);
return 0;
}

View File

@ -182,6 +182,10 @@ static struct rcu_head dummy;
static struct rcu_head *head = &dummy, **tail = &dummy.next;
static int rcu_call_count;
static QemuEvent rcu_call_ready_event;
static bool atfork_child_deferred;
static bool call_rcu_thread_started;
static void rcu_start_call_thread(void);
static void enqueue(struct rcu_head *node)
{
@ -290,6 +294,9 @@ void call_rcu1(struct rcu_head *node, void (*func)(struct rcu_head *node))
node->func = func;
enqueue(node);
qatomic_inc(&rcu_call_count);
if (!atfork_child_deferred) {
rcu_start_call_thread();
}
qemu_event_set(&rcu_call_ready_event);
}
@ -367,12 +374,17 @@ static void rcu_start_call_thread(void)
{
QemuThread thread;
if (qatomic_cmpxchg(&call_rcu_thread_started, false, true)) {
return;
}
qemu_thread_create(&thread, "call_rcu", call_rcu_thread,
NULL, QEMU_THREAD_DETACHED);
}
static void rcu_init_complete(bool start_thread)
{
qatomic_set(&call_rcu_thread_started, false);
qemu_mutex_init(&rcu_registry_lock);
qemu_mutex_init(&rcu_sync_lock);
qemu_event_init(&rcu_gp_event, true);
@ -387,7 +399,6 @@ static void rcu_init_complete(bool start_thread)
}
static int atfork_depth = 1;
static bool atfork_child_deferred;
void rcu_enable_atfork(void)
{
@ -422,7 +433,7 @@ void rcu_start_deferred_thread(void)
bool rcu_call_thread_is_running(void)
{
return !atfork_child_deferred;
return qatomic_read(&call_rcu_thread_started);
}
#ifdef CONFIG_POSIX
@ -457,7 +468,11 @@ static void rcu_init_child(void)
}
memset(&registry, 0, sizeof(registry));
#ifdef CONFIG_LATX
rcu_init_complete(false);
#else
rcu_init_complete(!atfork_child_deferred);
#endif
}
void rcu_raw_clone_prepare(void)
@ -484,10 +499,14 @@ static void __attribute__((__constructor__)) rcu_init(void)
#endif
#ifdef CONFIG_LATX
/*
* Chromium execs its PID namespace child before entering the nested user
* namespace. Keep that child single-threaded until the guest unshare.
* Keep translator-only threads out of a single-threaded guest until RCU
* work actually needs a consumer. Chromium additionally defers that
* on-demand start until its namespace child enters the nested user
* namespace.
*/
atfork_child_deferred = g_strcmp0(g_getenv("SBX_USER_NS"), "1") == 0;
rcu_init_complete(false);
#else
rcu_init_complete(true);
#endif
rcu_init_complete(!atfork_child_deferred);
}