LATX: guide users when guest runtimes fail to load

A PT_INTERP failure previously exposed only a low-level host error, leaving users unable to tell which guest ABI, runtime root, or loader path LATX had selected.

Report the selected runtime source, the guest PT_INTERP value, the configured candidate from the shared path resolver, the actually attempted host path, a stable failure category, and the underlying detail. Use QEMU error-reporting APIs and direct users to the companion latu-runtime-manager status command.

The diagnostic is emitted only at the interpreter load failure. Successful dynamic guests and static guests retain their existing behavior. Synthetic x86_64 and i386 fixtures cover absolute and relative interpreters, missing, unreadable, corrupt, truncated, valid dynamic, and static cases.

Signed-off-by: Hanlu Li <heuleehanlu@gmail.com>
This commit is contained in:
Hanlu Li 2026-08-04 14:44:45 +08:00 committed by Hanlu Li
parent 0863f52ad6
commit 2dafc11dfc
6 changed files with 311 additions and 2 deletions

View File

@ -105,6 +105,7 @@ jobs:
--disable-docs \
--with-git-submodules=ignore
cd ..
export LATX_RUNTIME_TEST_SKIP_GUEST_EXECUTION=1
ASAN_OPTIONS=detect_leaks=0 \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
python3 -B meson/meson.py test \

View File

@ -2970,25 +2970,85 @@ static bool load_elf_image(const char *image_name, const ImageSource *src,
exit(-1);
}
#ifdef CONFIG_LATX
static void latx_report_runtime_loader_failure(const char *filename,
const char *configured_path,
const char *attempted_path,
const char *reason,
const char *detail)
{
g_autofree char *escaped_abi = NULL;
g_autofree char *escaped_interp = NULL;
g_autofree char *escaped_root = NULL;
g_autofree char *escaped_configured = NULL;
g_autofree char *escaped_attempted = NULL;
g_autofree char *escaped_detail = NULL;
escaped_abi = g_strescape(latx_runtime_guest_abi(), NULL);
escaped_interp = g_strescape(filename, NULL);
escaped_root = g_strescape(interp_prefix, NULL);
escaped_configured = g_strescape(configured_path, NULL);
escaped_attempted = g_strescape(attempted_path, NULL);
escaped_detail = g_strescape(detail, NULL);
error_report("LATU: guest runtime loader failure");
error_printf(" guest ABI: %s\n"
" PT_INTERP: %s\n"
" runtime root: %s\n"
" runtime source: %s\n"
" configured loader: %s\n"
" attempted loader: %s\n"
" failure reason: %s\n"
" detail: %s\n"
" next step: run 'latu-runtime-manager status'\n",
escaped_abi, escaped_interp, escaped_root,
latx_runtime_prefix_source_name(), escaped_configured,
escaped_attempted, reason, escaped_detail);
}
#endif
static void load_elf_interp(const char *filename, struct image_info *info,
char bprm_buf[BPRM_BUF_SIZE])
{
struct elfhdr ehdr;
ImageSource src;
const char *loader_path = path(filename);
int fd, retval;
Error *err = NULL;
#ifdef CONFIG_LATX
g_autofree char *configured_path = path_get_prefixed(filename);
bool header_valid = false;
#endif
fd = open(path(filename), O_RDONLY);
fd = open(loader_path, O_RDONLY);
if (fd < 0) {
#ifdef CONFIG_LATX
int saved_errno = errno;
latx_report_runtime_loader_failure(
filename, configured_path, loader_path,
latx_runtime_loader_errno_name(saved_errno),
strerror(saved_errno));
#else
error_setg_file_open(&err, errno, filename);
error_report_err(err);
#endif
exit(-1);
}
retval = read(fd, bprm_buf, BPRM_BUF_SIZE);
if (retval < 0) {
#ifdef CONFIG_LATX
int saved_errno = errno;
latx_report_runtime_loader_failure(
filename, configured_path, loader_path,
latx_runtime_loader_errno_name(saved_errno),
strerror(saved_errno));
#else
error_setg_errno(&err, errno, "Error reading file header");
error_reportf_err(err, "%s: ", filename);
#endif
exit(-1);
}
@ -2996,8 +3056,27 @@ static void load_elf_interp(const char *filename, struct image_info *info,
src.cache = bprm_buf;
src.cache_size = retval;
#ifdef CONFIG_LATX
if ((size_t)retval >= sizeof(ehdr)) {
memcpy(&ehdr, bprm_buf, sizeof(ehdr));
header_valid = elf_check_ident(&ehdr);
if (header_valid) {
bswap_ehdr(&ehdr);
header_valid = elf_check_ehdr(&ehdr);
}
}
#endif
if (!load_elf_image(filename, &src, info, &ehdr, NULL, &err)) {
#ifdef CONFIG_LATX
latx_report_runtime_loader_failure(
filename, configured_path, loader_path,
header_valid ? "load_error" : "invalid_elf",
error_get_pretty(err));
error_free(err);
#else
error_reportf_err(err, "%s: ", filename);
#endif
exit(-1);
}
close(fd);

View File

@ -20,5 +20,6 @@ void latx_runtime_option_source_set(LatxRuntimeSource source);
void latx_runtime_prefix_selected(void);
const char *latx_runtime_prefix_source_name(void);
const char *latx_runtime_guest_abi(void);
const char *latx_runtime_loader_errno_name(int errnum);
#endif

View File

@ -52,3 +52,15 @@ const char *latx_runtime_guest_abi(void)
return "i386";
#endif
}
const char *latx_runtime_loader_errno_name(int errnum)
{
switch (errnum) {
case ENOENT:
return "not_found";
case EACCES:
return "permission_denied";
default:
return "io_error";
}
}

View File

@ -66,6 +66,11 @@ static void test_runtime_prefix_source(void)
#else
g_assert_cmpstr(latx_runtime_guest_abi(), ==, "i386");
#endif
g_assert_cmpstr(latx_runtime_loader_errno_name(ENOENT), ==, "not_found");
g_assert_cmpstr(latx_runtime_loader_errno_name(EACCES), ==,
"permission_denied");
g_assert_cmpstr(latx_runtime_loader_errno_name(EIO), ==, "io_error");
}
static void test_user_config_path(void)

View File

@ -41,6 +41,82 @@ if actual != expected:
fail "unexpected $expected_source runtime information"
}
assert_stderr()
{
expected=$1
stderr_file=$2
context=$3
grep -F -- "$expected" "$stderr_file" > /dev/null ||
fail "$context: missing '$expected'"
}
make_elf()
{
elf_kind=$1
elf_path=$2
elf_interp=$3
elf_base=$4
"$python" - "$guest_abi" "$elf_kind" "$elf_path" \
"$elf_interp" "$elf_base" <<'PY'
import struct
import sys
abi, kind, output, interpreter, base_arg = sys.argv[1:]
base = int(base_arg, 0)
elf_type = 3 if kind == "loader" else 2
if abi == "x86_64":
elf_class = 2
machine = 62
ehsize = 64
phentsize = 56
code = bytes.fromhex("b83c00000031ff0f05")
pack_ehdr = lambda ident, entry, phnum: struct.pack(
"<16sHHIQQQIHHHHHH", ident, elf_type, machine, 1, entry, ehsize, 0,
0, ehsize, phentsize, phnum, 0, 0, 0)
pack_load = lambda size: struct.pack(
"<IIQQQQQQ", 1, 5, 0, base, base, size, size, 0x1000)
pack_interp = lambda offset, size: struct.pack(
"<IIQQQQQQ", 3, 4, offset, 0, 0, size, size, 1)
elif abi == "i386":
elf_class = 1
machine = 3
ehsize = 52
phentsize = 32
code = bytes.fromhex("b80100000031dbcd80")
pack_ehdr = lambda ident, entry, phnum: struct.pack(
"<16sHHIIIIIHHHHHH", ident, elf_type, machine, 1, entry, ehsize, 0,
0, ehsize, phentsize, phnum, 0, 0, 0)
pack_load = lambda size: struct.pack(
"<IIIIIIII", 1, 0, base, base, size, size, 5, 0x1000)
pack_interp = lambda offset, size: struct.pack(
"<IIIIIIII", 3, offset, 0, 0, size, size, 4, 1)
else:
raise SystemExit(f"unsupported guest ABI: {abi}")
ident = b"\x7fELF" + bytes([elf_class, 1, 1, 0]) + bytes(8)
interp_data = interpreter.encode() + b"\0" if kind == "dynamic" else b""
phnum = 2 if interp_data else 1
interp_offset = ehsize + phentsize * phnum
code_offset = interp_offset + len(interp_data)
total_size = code_offset + len(code)
ehdr = pack_ehdr(ident, base + code_offset, phnum)
phdrs = pack_load(total_size)
if interp_data:
phdrs += pack_interp(interp_offset, len(interp_data))
with open(output, "wb") as f:
if kind == "truncated":
f.write(ehdr)
else:
f.write(ehdr + phdrs + interp_data + code)
PY
chmod +x "$elf_path"
}
case "$guest_abi" in
x86_64) config_name=latx-x86_64.conf ;;
i386) config_name=latx-i386.conf ;;
@ -82,4 +158,139 @@ output=$(HOME=$test_home LAT_VERSION=1 LATX_AVX_CPUID=1 \
fail 'runtime info with a diagnostic option failed'
assert_info "$workdir/global-root" user_config "$output"
echo "PASS: $guest_abi runtime selection information"
case "$guest_abi" in
x86_64)
main_base=0x400000
;;
i386)
main_base=0x08048000
;;
esac
empty_home=$workdir/empty-home
mkdir -p "$empty_home"
guest_loader=/latu-test/actual-$guest_abi-loader.so
dynamic_guest=$workdir/dynamic-$guest_abi
static_guest=$workdir/static-$guest_abi
make_elf dynamic "$dynamic_guest" "$guest_loader" "$main_base"
make_elf static "$static_guest" '' "$main_base"
missing_root=$workdir/missing-root
mkdir -p "$missing_root"
set +e
HOME=$empty_home "$translator" -L "$missing_root" "$dynamic_guest" \
> "$workdir/missing.stdout" 2> "$workdir/missing.stderr"
missing_status=$?
set -e
[ "$missing_status" -ne 0 ] || fail 'missing loader unexpectedly succeeded'
assert_stderr 'LATU: guest runtime loader failure' \
"$workdir/missing.stderr" 'missing loader'
assert_stderr "guest ABI: $guest_abi" \
"$workdir/missing.stderr" 'missing loader'
assert_stderr "PT_INTERP: $guest_loader" \
"$workdir/missing.stderr" 'missing loader'
assert_stderr "runtime root: $missing_root" \
"$workdir/missing.stderr" 'missing loader'
assert_stderr 'runtime source: command_line' \
"$workdir/missing.stderr" 'missing loader'
assert_stderr "configured loader: $missing_root$guest_loader" \
"$workdir/missing.stderr" 'missing loader'
assert_stderr "attempted loader: $guest_loader" \
"$workdir/missing.stderr" 'missing loader'
assert_stderr 'failure reason: not_found' \
"$workdir/missing.stderr" 'missing loader'
assert_stderr "next step: run 'latu-runtime-manager status'" \
"$workdir/missing.stderr" 'missing loader'
relative_loader=latu-test/missing-$guest_abi-loader.so
relative_guest=$workdir/relative-$guest_abi
make_elf dynamic "$relative_guest" "$relative_loader" "$main_base"
set +e
(
cd "$workdir"
HOME=$empty_home "$translator" -L "$missing_root" "$relative_guest"
) > "$workdir/relative.stdout" 2> "$workdir/relative.stderr"
relative_status=$?
set -e
[ "$relative_status" -ne 0 ] || fail 'relative loader unexpectedly succeeded'
assert_stderr "PT_INTERP: $relative_loader" \
"$workdir/relative.stderr" 'relative loader'
assert_stderr "configured loader: $relative_loader" \
"$workdir/relative.stderr" 'relative loader'
assert_stderr "attempted loader: $relative_loader" \
"$workdir/relative.stderr" 'relative loader'
assert_stderr 'failure reason: not_found' \
"$workdir/relative.stderr" 'relative loader'
corrupt_root=$workdir/corrupt-root
mkdir -p "$corrupt_root$(dirname "$guest_loader")"
printf 'not an ELF loader\n' > "$corrupt_root$guest_loader"
set +e
HOME=$empty_home "$translator" -L "$corrupt_root" "$dynamic_guest" \
> "$workdir/corrupt.stdout" 2> "$workdir/corrupt.stderr"
corrupt_status=$?
set -e
[ "$corrupt_status" -ne 0 ] || fail 'corrupt loader unexpectedly succeeded'
assert_stderr "PT_INTERP: $guest_loader" \
"$workdir/corrupt.stderr" 'corrupt loader'
assert_stderr "configured loader: $corrupt_root$guest_loader" \
"$workdir/corrupt.stderr" 'corrupt loader'
assert_stderr "attempted loader: $corrupt_root$guest_loader" \
"$workdir/corrupt.stderr" 'corrupt loader'
assert_stderr 'failure reason: invalid_elf' \
"$workdir/corrupt.stderr" 'corrupt loader'
load_error_root=$workdir/load-error-root
mkdir -p "$load_error_root$(dirname "$guest_loader")"
make_elf truncated "$load_error_root$guest_loader" '' 0
set +e
HOME=$empty_home "$translator" -L "$load_error_root" "$dynamic_guest" \
> "$workdir/load-error.stdout" 2> "$workdir/load-error.stderr"
load_error_status=$?
set -e
[ "$load_error_status" -ne 0 ] || fail 'truncated loader unexpectedly succeeded'
assert_stderr "attempted loader: $load_error_root$guest_loader" \
"$workdir/load-error.stderr" 'truncated loader'
assert_stderr 'failure reason: load_error' \
"$workdir/load-error.stderr" 'truncated loader'
permission_root=$workdir/permission-root
mkdir -p "$permission_root$(dirname "$guest_loader")"
: > "$permission_root$guest_loader"
chmod 000 "$permission_root$guest_loader"
if [ "$(id -u)" -ne 0 ]; then
set +e
HOME=$empty_home "$translator" -L "$permission_root" "$dynamic_guest" \
> "$workdir/permission.stdout" 2> "$workdir/permission.stderr"
permission_status=$?
set -e
[ "$permission_status" -ne 0 ] ||
fail 'unreadable loader unexpectedly succeeded'
assert_stderr 'failure reason: permission_denied' \
"$workdir/permission.stderr" 'unreadable loader'
fi
if [ "${LATX_RUNTIME_TEST_SKIP_GUEST_EXECUTION:-0}" = 1 ]; then
echo "SKIP: $guest_abi translated guest execution"
else
success_root=$workdir/success-root
mkdir -p "$success_root$(dirname "$guest_loader")"
make_elf loader "$success_root$guest_loader" '' 0
HOME=$empty_home "$translator" -L "$success_root" "$dynamic_guest" \
> "$workdir/success.stdout" 2> "$workdir/success.stderr" ||
fail 'valid runtime loader failed'
if grep -F 'LATU: guest runtime loader failure' \
"$workdir/success.stderr" > /dev/null; then
fail 'valid runtime loader emitted failure guidance'
fi
HOME=$empty_home "$translator" -L "$missing_root" "$static_guest" \
> "$workdir/static.stdout" 2> "$workdir/static.stderr" ||
fail 'static guest failed without a runtime'
if grep -F 'LATU: guest runtime loader failure' \
"$workdir/static.stderr" > /dev/null; then
fail 'static guest emitted runtime failure guidance'
fi
fi
echo "PASS: $guest_abi runtime selection and loader guidance"