Skip to content

linux-user: retry transient private futex faults - #461

Open
wojiushixiaobai wants to merge 1 commit into
lat-opensource:masterfrom
wojiushixiaobai:master
Open

linux-user: retry transient private futex faults#461
wojiushixiaobai wants to merge 1 commit into
lat-opensource:masterfrom
wojiushixiaobai:master

Conversation

@wojiushixiaobai

@wojiushixiaobai wojiushixiaobai commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Handle transient EFAULT from i386 private FUTEX_WAIT* calls while
LATX temporarily relocates a host page. Return EAGAIN only when QEMU's
guest page metadata still marks the futex address valid and readable, so
userspace reloads the futex word and retries while unmapped or unreadable
guest addresses retain EFAULT.

Fixes #87

@wojiushixiaobai

wojiushixiaobai commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author
8e785b816b7e462aa0300a3976f3d332
root@debian:/opt/lat# cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
VERSION_CODENAME=trixie
DEBIAN_VERSION_FULL=13.6
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
root@debian:/opt/lat# uname -a
Linux debian 6.12.107+deb13-loong64 #1 SMP Debian 6.12.107-1 (2026-08-29) loongarch64 GNU/Linux
root@debian:/opt/lat# cat /usr/local/bin/steam 
#!/bin/sh
# Run Steam's x86 client through LATX with the compatible system runtime.
export STEAMOS=1
# Keep pressure-vessel's native bubblewrap outside the translated Steam
# runtime.  This wrapper adds the WebHelper workaround without modifying
# files that Steam's updater owns.
export PRESSURE_VESSEL_BWRAP=/usr/local/libexec/steam-bwrap
# The translated 32-bit client crashes in guest radeonsi without this.
export LIBGL_ALWAYS_SOFTWARE=1
# Chromium forks zygote, GPU and network utility processes.  Unlink the
# translator's cached VCPU chains before fork so each child re-execs cleanly.
export LATX_FORK_UNLINK=1

# Steam's WebHelper is sandboxed.  Do not allow a graphical launcher started
# through privilege/session wrappers to leak another user's D-Bus or audio
# sockets into that sandbox.
steam_uid=$(id -u)
if test -d "/run/user/${steam_uid}"; then
    export XDG_RUNTIME_DIR="/run/user/${steam_uid}"
fi
if test -S "${XDG_RUNTIME_DIR}/bus"; then
    export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
fi
if test -S "${XDG_RUNTIME_DIR}/pulse/native"; then
    export PULSE_SERVER="unix:${XDG_RUNTIME_DIR}/pulse/native"
fi
exec /usr/bin/steam -cef-disable-gpu "$@"
root@debian:/opt/lat# cat /usr/local/libexec/steam-bwrap
#!/bin/sh
# Pressure-vessel invokes this wrapper as its bubblewrap implementation.
# LATX's binfmt registration retains the guest ELF fd, so CEF utility
# processes can use their normal sandbox and process model.
exec /usr/bin/bwrap "$@"

Comment thread linux-user/syscall.c Outdated
* remains readable across that host-only move, unlike an invalid guest
* futex address. Check it under mmap_lock to retain genuine EFAULTs.
*/
if (qemu_host_page_size <= TARGET_PAGE_SIZE || ret != -TARGET_EFAULT ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4K页也有相同的问题,可以同时支持

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

由于我本地并没有 4k 内核的环境,我稍后重新提交一版再帮忙审查一下。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

新的commit中加了对pressure-vessel的支持,这里应该暂时不需要了

@wojiushixiaobai wojiushixiaobai Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

合并 #458 后 steam 无法启动,我检查下哪里缺失。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

直接STEAMOS=1 steam可以启动吗

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

有复现环境吗?我目前的运行环境:
系统:aosc 13.3.1
runtime:runtime
steam安装包:官网下载
运行命令:STEAMOS=1 LATX_KZT=0 usr/bin/steam

This comment was marked as outdated.

@wojiushixiaobai wojiushixiaobai Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

有复现环境吗?我目前的运行环境: 系统:aosc 13.3.1 runtime:runtime steam安装包:官网下载 运行命令:STEAMOS=1 LATX_KZT=0 usr/bin/steam

Debian 13,没有使用 runtime 而是通过 dpkg --add-architecture 的方式添加 i386amd64 架构安装所需的依赖。

cat > /etc/apt/sources.list.d/trixie.sources << "EOF"
Types: deb
# http://snapshot.debian.org/archive/debian/20260824T000000Z
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp

Types: deb
# http://snapshot.debian.org/archive/debian-security/20260824T000000Z
URIs: http://deb.debian.org/debian-security
Suites: trixie-security
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp
EOF

dpkg --add-architecture i386
dpkg --add-architecture amd64

# 修改 steam.deb DEBIAN/control 重新打包安装。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

我这暂时没有复现这个问题...

@baibaidashixiong baibaidashixiong Sep 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

可以先合fe9c44cfc7f51e8002454213bc36e631e3b0a347这个commit,后面两个commit先撤掉吧

@wojiushixiaobai wojiushixiaobai changed the title fix(linux-user): preserve real futex EFAULT on 16 KiB hosts linux-user: retry transient private futex faults Sep 4, 2026
Signed-off-by: 吴小白 <296015668@qq.com>
@LaurenIsACoder

Copy link
Copy Markdown
Contributor

感谢修复!我这边测到一个边界问题。

先 mmap 一个文件,再用 ftruncate(fd, 0) 把文件清空,之后对这个地址调用 private futex WAIT。原生 x86 返回 EFAULT,这版会返回 EAGAIN。我用同一个 i386 程序在 16K 页的 LoongArch 上测的,WAIT、WAIT_BITSET,以及 futex_time64 对应的两个调用都能复现。

问题应该就在 sanitize_private_futex_fault() 这里:文件清空了,PAGE_VALID | PAGE_READ 还在,但内容已经读不了了。这种情况再试也没用,还是应该返回 EFAULT

复现程序放下面了,不需要 32 位 libc。

repro.c
/* Freestanding i386 regression: no guest libc or multilib headers needed. */
typedef unsigned int u32;
extern int sc(int nr, u32 a, u32 b, u32 c, u32 d, u32 e, u32 f);
__asm__(
    ".text\n.globl sc\nsc:\n"
    "push %ebp; push %edi; push %esi; push %ebx;\n"
    "mov 20(%esp), %eax; mov 24(%esp), %ebx;\n"
    "mov 28(%esp), %ecx; mov 32(%esp), %edx;\n"
    "mov 36(%esp), %esi; mov 40(%esp), %edi;\n"
    "mov 44(%esp), %ebp; int $0x80;\n"
    "pop %ebx; pop %esi; pop %edi; pop %ebp; ret;\n");
#define PTR(p) ((u32)(p))
static void say(const char *s)
{
    u32 n = 0;
    while (s[n]) { n++; }
    sc(4, 1, PTR(s), n, 0, 0, 0);
}
static void number(int n)
{
    char buf[16];
    unsigned int v = n < 0 ? -n : n;
    unsigned int i = sizeof(buf);
    do { buf[--i] = '0' + v % 10; v /= 10; } while (v);
    if (n < 0) { buf[--i] = '-'; }
    sc(4, 1, PTR(buf + i), sizeof(buf) - i, 0, 0, 0);
}
static void die(const char *s, int ret)
{
    say(s); number(ret); say("\n");
    sc(1, 2, 0, 0, 0, 0, 0);
    __builtin_unreachable();
}
void _start(void)
{
    const char path[] = "futex-truncate.data";
    const u32 len = 65536;
    const unsigned long long zero[2] = {0, 0};
    const int syscalls[2] = {240, 422}; /* futex, futex_time64 */
    const int ops[2] = {128, 137}; /* PRIVATE WAIT, PRIVATE WAIT_BITSET */
    int fd = sc(5, PTR(path), 0xc2, 0600, 0, 0, 0);
    if (fd < 0) { die("open: ", fd); }
    int ret = sc(10, PTR(path), 0, 0, 0, 0, 0);
    if (ret) { die("unlink: ", ret); }
    ret = sc(93, fd, len, 0, 0, 0, 0);
    if (ret) { die("ftruncate: ", ret); }
    u32 p = sc(192, 0, len, 3, 2, fd, 0); /* mmap2, RW, MAP_PRIVATE */
    if (p >= (u32)-4095) { die("mmap2: ", p); }
    ret = sc(93, fd, 0, 0, 0, 0, 0);
    if (ret) { die("truncate zero: ", ret); }
    int failed = 0;
    /* No userspace access to p: only the kernel reads the futex word. */
    for (u32 i = 0; i < 2; i++) {
        for (u32 j = 0; j < 2; j++) {
            ret = sc(syscalls[i], p, ops[j], 0, PTR(zero), 0, 0xffffffff);
            say("syscall="); number(syscalls[i]);
            say(" op="); number(ops[j]); say(" result="); number(ret);
            say(" expected=-14 "); say(ret == -14 ? "PASS\n" : "FAIL\n");
            failed |= ret != -14;
        }
    }
    sc(91, p, len, 0, 0, 0, 0);
    sc(6, fd, 0, 0, 0, 0, 0);
    sc(1, failed, 0, 0, 0, 0, 0);
    __builtin_unreachable();
}

在 x86 Linux 上编译:

gcc -m32 -nostdlib -static -fno-pie -no-pie -fno-stack-protector \
    -fno-builtin -O2 -Wall -Wextra -Werror repro.c -o repro
./repro

再把 repro 拷到 LoongArch,在可写目录里跑:

timeout 15s /path/to/latx-i386 ./repro

原生四项都返回 -14(EFAULT),这版的结果是:

syscall=240 op=128 result=-11 expected=-14 FAIL
syscall=240 op=137 result=-11 expected=-14 FAIL
syscall=422 op=128 result=-11 expected=-14 FAIL
syscall=422 op=137 result=-11 expected=-14 FAIL

我的想法是保留现在的锁和页检查,在锁内再确认一下这 4 字节能不能读,读到了才改成 EAGAIN。下面写了个用 process_vm_readv 的参考改法,避免直接解引用这个地址。

参考 diff
--- a/linux-user/syscall.c
+++ b/linux-user/syscall.c
@@ -11061,25 +11061,34 @@
      * The i386 lock-instruction interpreter transiently moves a complete
      * host page.  A private futex in that page can observe the short gap as
      * EFAULT, including when host and target pages are both 4 KiB.  The guest
-     * mapping metadata persists across that host-only move.  Check it under
-     * mmap_lock so unmapped or unreadable guest futex addresses retain
-     * EFAULT.  For a mapped private WAIT, return EAGAIN so userspace reloads
-     * the futex word and retries.
+     * mapping metadata persists across that host-only move.  Taking
+     * mmap_lock waits for relocation to finish.  Also probe the actual
+     * word: readable guest metadata can survive truncation of a mapped
+     * file.  Only a successful read permits returning EAGAIN.
      */
     if (ret != -TARGET_EFAULT || !(op & FUTEX_PRIVATE_FLAG)) {
         return ret;
     }
     base_op = op & FUTEX_CMD_MASK;
-    if (!h2g_valid(uaddr)) {
+    if ((base_op != FUTEX_WAIT && base_op != FUTEX_WAIT_BITSET) ||
+        !h2g_valid(uaddr)) {
         return ret;
     }
     mmap_lock();
     mapped = (page_get_flags(h2g(uaddr)) & (PAGE_VALID | PAGE_READ)) ==
              (PAGE_VALID | PAGE_READ);
+    if (mapped) {
+        uint32_t word;
+        struct iovec local = { .iov_base = &word, .iov_len = sizeof(word) };
+        struct iovec remote = { .iov_base = uaddr, .iov_len = sizeof(word) };
+
+        /* Let the kernel report inaccessible backing memory as an error. */
+        if (process_vm_readv(getpid(), &local, 1, &remote, 1, 0) ==
+            sizeof(word)) {
+            ret = -TARGET_EAGAIN;
+        }
+    }
     mmap_unlock();
-    if ((base_op == FUTEX_WAIT || base_op == FUTEX_WAIT_BITSET) && mapped) {
-        return -TARGET_EAGAIN;
-    }
 #endif
     return ret;
 }

不过这个改法我还没编译实测,process_vm_readv 在 Steam 沙箱里能不能用也得确认一下。错误返回已经复现了,具体怎么修可以再商量。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: debian系统启动steam失败

3 participants