From 0a0f552995ede21723c1fabe202bd40ffa8fc3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Thu, 3 Sep 2026 21:12:30 +0800 Subject: [PATCH 1/2] linux-user: retry transient private futex faults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 吴小白 <296015668@qq.com> --- linux-user/syscall.c | 46 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/linux-user/syscall.c b/linux-user/syscall.c index 52d225705c..43e9444928 100644 --- a/linux-user/syscall.c +++ b/linux-user/syscall.c @@ -11052,6 +11052,39 @@ static int do_sys_futex(int *uaddr, int op, int val, g_assert_not_reached(); } +static int sanitize_private_futex_fault(int ret, int *uaddr, int op) +{ +#if defined(CONFIG_LATX) && defined(TARGET_I386) && !defined(TARGET_X86_64) + int base_op; + bool mapped = false; + + /* + * 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. + */ + if (ret != -TARGET_EFAULT || !(op & FUTEX_PRIVATE_FLAG)) { + return ret; + } + base_op = op & FUTEX_CMD_MASK; + if (!h2g_valid(uaddr)) { + return ret; + } + mmap_lock(); + mapped = (page_get_flags(h2g(uaddr)) & (PAGE_VALID | PAGE_READ)) == + (PAGE_VALID | PAGE_READ); + mmap_unlock(); + if ((base_op == FUTEX_WAIT || base_op == FUTEX_WAIT_BITSET) && mapped) { + return -TARGET_EAGAIN; + } +#endif + return ret; +} + static int do_safe_futex(int *uaddr, int op, int val, const struct timespec *timeout, int *uaddr2, int val3) @@ -11059,19 +11092,24 @@ static int do_safe_futex(int *uaddr, int op, int val, #if HOST_LONG_BITS == 64 #if defined(__NR_futex) /* always a 64-bit time_t, it doesn't define _time64 version */ - return get_errno(safe_futex(uaddr, op, val, timeout, uaddr2, val3)); + return sanitize_private_futex_fault( + get_errno(safe_futex(uaddr, op, val, timeout, uaddr2, val3)), uaddr, + op); #endif #else /* HOST_LONG_BITS == 64 */ #if defined(__NR_futex_time64) if (sizeof(timeout->tv_sec) == 8) { /* _time64 function on 32bit arch */ - return get_errno(safe_futex_time64(uaddr, op, val, timeout, uaddr2, - val3)); + return sanitize_private_futex_fault( + get_errno(safe_futex_time64(uaddr, op, val, timeout, uaddr2, + val3)), uaddr, op); } #endif #if defined(__NR_futex) /* old function on 32bit arch */ - return get_errno(safe_futex(uaddr, op, val, timeout, uaddr2, val3)); + return sanitize_private_futex_fault( + get_errno(safe_futex(uaddr, op, val, timeout, uaddr2, val3)), uaddr, + op); #endif #endif /* HOST_LONG_BITS == 64 */ return -TARGET_ENOSYS; From ce77b513f0793e27e6d7ae73e5cb4717287117c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Sat, 12 Sep 2026 13:28:54 +0800 Subject: [PATCH 2/2] linux-user: preserve futex EFAULT for truncated private mappings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 吴小白 <296015668@qq.com> --- linux-user/syscall.c | 27 ++++-- .../integration/futex-private-truncate-i386.c | 89 +++++++++++++++++++ .../registrations/process/meson.build | 8 ++ .../test-futex-private-truncate-i386.sh | 36 ++++++++ 4 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 tests/integration/futex-private-truncate-i386.c create mode 100755 tests/integration/test-futex-private-truncate-i386.sh diff --git a/linux-user/syscall.c b/linux-user/syscall.c index 43e9444928..c9bcbb181a 100644 --- a/linux-user/syscall.c +++ b/linux-user/syscall.c @@ -11056,31 +11056,40 @@ static int sanitize_private_futex_fault(int ret, int *uaddr, int op) { #if defined(CONFIG_LATX) && defined(TARGET_I386) && !defined(TARGET_X86_64) int base_op; - bool mapped = false; + bool mapped; /* * 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); - mmap_unlock(); - if ((base_op == FUTEX_WAIT || base_op == FUTEX_WAIT_BITSET) && mapped) { - return -TARGET_EAGAIN; + 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(); #endif return ret; } diff --git a/tests/integration/futex-private-truncate-i386.c b/tests/integration/futex-private-truncate-i386.c new file mode 100644 index 0000000000..bcb58138e5 --- /dev/null +++ b/tests/integration/futex-private-truncate-i386.c @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Freestanding i386 regression for private futexes on truncated file maps. */ +typedef unsigned int u32; + +extern int syscall6(int nr, u32 a, u32 b, u32 c, u32 d, u32 e, u32 f); + +__asm__( + ".text\n.globl syscall6\nsyscall6:\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)) + +#define NR_OPEN 5 +#define NR_CLOSE 6 +#define NR_UNLINK 10 +#define NR_MUNMAP 91 +#define NR_FTRUNCATE 93 +#define NR_MMAP2 192 +#define NR_EXIT 1 +#define NR_FUTEX 240 +#define NR_FUTEX_TIME64 422 + +#define O_CREAT 0100 +#define O_EXCL 0200 +#define O_RDWR 2 + +#define PROT_READ 1 +#define PROT_WRITE 2 +#define MAP_PRIVATE 2 + +#define FUTEX_WAIT_PRIVATE 128 +#define FUTEX_WAIT_BITSET_PRIVATE 137 +#define EFAULT 14 + +void _start(void) +{ + static const char path[] = "futex-truncate.data"; + static const unsigned long long zero[2]; + static const int syscalls[] = { NR_FUTEX, NR_FUTEX_TIME64 }; + static const int ops[] = { FUTEX_WAIT_PRIVATE, + FUTEX_WAIT_BITSET_PRIVATE }; + const u32 length = 65536; + int failed = 0; + int fd; + int ret; + u32 address; + u32 i; + u32 j; + + fd = syscall6(NR_OPEN, PTR(path), O_CREAT | O_EXCL | O_RDWR, 0600, + 0, 0, 0); + if (fd < 0) { + failed = 1; + goto out; + } + if (syscall6(NR_UNLINK, PTR(path), 0, 0, 0, 0, 0) || + syscall6(NR_FTRUNCATE, fd, length, 0, 0, 0, 0)) { + failed = 1; + goto close; + } + address = syscall6(NR_MMAP2, 0, length, PROT_READ | PROT_WRITE, + MAP_PRIVATE, fd, 0); + if (address >= (u32)-4095 || + syscall6(NR_FTRUNCATE, fd, 0, 0, 0, 0, 0)) { + failed = 1; + goto close; + } + + /* Only the kernel accesses address: it must reject the truncated page. */ + for (i = 0; i < sizeof(syscalls) / sizeof(syscalls[0]); i++) { + for (j = 0; j < sizeof(ops) / sizeof(ops[0]); j++) { + ret = syscall6(syscalls[i], address, ops[j], 0, PTR(zero), 0, + 0xffffffff); + failed |= ret != -EFAULT; + } + } + syscall6(NR_MUNMAP, address, length, 0, 0, 0, 0); + +close: + syscall6(NR_CLOSE, fd, 0, 0, 0, 0, 0); +out: + syscall6(NR_EXIT, failed, 0, 0, 0, 0, 0); + __builtin_unreachable(); +} diff --git a/tests/integration/registrations/process/meson.build b/tests/integration/registrations/process/meson.build index eddba40a6b..3b458f68e6 100644 --- a/tests/integration/registrations/process/meson.build +++ b/tests/integration/registrations/process/meson.build @@ -86,6 +86,14 @@ if 'x86_64-linux-user' in target_dirs endif if 'i386-linux-user' in target_dirs + latx_integration_tests += [{ + 'name': 'test-futex-private-truncate-i386', + 'runner': find_program('../../test-futex-private-truncate-i386.sh'), + 'args': [ + emulators['latx-i386'], + files('../../futex-private-truncate-i386.c'), + ], + }] latx_integration_tests += [{ 'name': 'test-softfpu-eflags-region-i386', 'runner': find_program('../../test-softfpu-eflags-region-i386.sh'), diff --git a/tests/integration/test-futex-private-truncate-i386.sh b/tests/integration/test-futex-private-truncate-i386.sh new file mode 100755 index 0000000000..4711beedae --- /dev/null +++ b/tests/integration/test-futex-private-truncate-i386.sh @@ -0,0 +1,36 @@ +#!/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 i386 guest" + exit 77 +fi + +"$clang" --target=i386-linux-gnu -fuse-ld=lld -nostdlib -static -no-pie \ + -O2 -ffreestanding -fno-builtin -fno-pie -fno-stack-protector \ + -Wl,--build-id=none \ + "$source_file" -o "$workdir/futex-private-truncate-i386" + +set +e +LATX_AOT=0 LATX_KZT=0 timeout -s KILL 15 \ + "$emulator" "$workdir/futex-private-truncate-i386" +ret=$? +set -e + +case $ret in +0) echo "PASS: i386 private futexes fault on truncated file mappings" ;; +1) echo "FAIL: private futex returned a non-EFAULT after truncation" >&2 ;; +124) echo "FAIL: i386 private futex truncation test timed out" >&2 ;; +*) echo "FAIL: unexpected i386 futex test exit status $ret" >&2 ;; +esac + +test "$ret" -eq 0