Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions linux-user/syscall.c
Original file line number Diff line number Diff line change
Expand Up @@ -11052,26 +11052,73 @@ 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;

/*
* 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. 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 ((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();
#endif
return ret;
}

static int do_safe_futex(int *uaddr, int op, int val,
const struct timespec *timeout, int *uaddr2,
int val3)
{
#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;
Expand Down
89 changes: 89 additions & 0 deletions tests/integration/futex-private-truncate-i386.c
Original file line number Diff line number Diff line change
@@ -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();
}
8 changes: 8 additions & 0 deletions tests/integration/registrations/process/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
36 changes: 36 additions & 0 deletions tests/integration/test-futex-private-truncate-i386.sh
Original file line number Diff line number Diff line change
@@ -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
Loading