Skip to content

Commit e797e99

Browse files
morningmanclaude
andauthored
[fix](thirdparty) Upgrade libunwind to 1.8.3 to fix aarch64 BE startup crash on 64 KiB-page kernels (#67877)
### What problem does this PR solve? Related PR: #64093 Problem Summary: **Every aarch64 BE built from master or branch-4.1 (since 4.1.4-rc01) crashes before `main()` on a 64 KiB-page kernel** (Kunpeng 920 + Kylin V10 / openEuler / CentOS aarch64, the common ARM server stack). The core from a 4.1.4 arm64 deployment shows: ``` access_mem (addr=0) src/aarch64/Ginit.c is_plt_entry src/aarch64/Gstep.c _ULaarch64_step _ULaarch64_tdep_trace unw_backtrace <bthread/mutex.cpp static initializer: backtrace(dummy_buf, 4)> ← bRPC "Warm up backtrace before main()" __libc_csu_init ``` SIGSEGV with `si_addr=0`, AUXV `PAGESZ=65536`, and inside the libunwind cursor `ip=0`, `validate=0`, `mem_validate_func=msync_validate`, `last_good_addr[]={0,0,0,0}`. How it happens: 1. #64093 links GNU libunwind statically into the aarch64 BE (it forces `USE_UNWIND=ON` on Linux, references `unw_*` from `phdr_cache.cpp` without the `__x86_64__` guard, and builds jemalloc with `--enable-prof-libunwind`). libunwind's weak `backtrace` alias then replaces glibc's `backtrace()` for every caller in the process: bRPC's pre-main warm-up, `StackTrace::tryCapture()` on aarch64, jemalloc heap profiling. The 4.1.3 arm64 package was immune only because it was built with `USE_UNWIND=OFF` (which the ARM compilation doc still prescribes and #64093 removed): it has `backtrace@GLIBC_2.17` and zero `_ULaarch64_*` symbols. 2. libunwind 1.6.2 hard-codes `PAGE_SIZE 4096` in `src/aarch64/Ginit.c`. On a 64 KiB-page kernel `mincore()`/`msync()` reject its 4 KiB-aligned probes with `EINVAL`, so `validate_mem()` fails for 15/16 of all addresses (the core's `msync_validate` + empty `last_good_addr` are exactly this). 3. `unw_step()` therefore gets `unw_is_signal_frame() < 0` for a valid IP, takes the "IP points to non-mapped memory, use LR" recovery path and loads `uc.regs[30]` — a slot `_Uaarch64_getcontext_trace` never writes (it saves only FP/SP/PC). On the reporting host that stack garbage is 0. 4. `dwarf_step(ip=0)` finds nothing, and the fallback calls `is_plt_entry()` with `c->validate` restored to 0, so `access_mem` executes `ldr x8, [x1]` with `x1=0`. The PHDR cache being empty before `main()` is not what breaks this (step 3 has already poisoned `ip` before any FDE lookup, and the same crash reproduces without the Doris hook); changing the cache initialisation order would not fix it, and every later `backtrace()` on such a host rolls the same dice. Fix: **upgrade libunwind 1.6.2 → 1.8.3**, which was never bumped since it was introduced in 2023 (#21938): - 1.7.0+: `unw_page_size = sysconf(_SC_PAGESIZE)` instead of the hard-coded 4096. - 1.8.0+: `src/mi/Gaddress_validator.c` validates through the pipe-write probe only, so `mincore`/`msync` alignment no longer matters at all; `unw_step()` sets `c->validate = 1` before the DWARF-failure fallback, so a garbage IP is rejected instead of dereferenced. Changes: - `thirdparty/vars.sh`: libunwind 1.6.2 → 1.8.3 (md5 `13bc7b41462ac6ea157d350eaf6c1503`). - `thirdparty/patches/libunwind-1.8.3-doris-phdr-cache.patch`: the #64093 hook rebased onto 1.8.3's `as->iterate_phdr_function` call site; semantics unchanged (`doris_unwind_iterate_phdr` still takes precedence whenever it is linked in). 1.8 also exposes `unw_set_iterate_phdr_function()`, which could replace the source patch in a follow-up. - `thirdparty/build-thirdparty.sh`: `--disable-tests --disable-documentation`; only `libunwind.a` is consumed. - `thirdparty/test/libunwind-page-size-test.sh` (+ run at the end of the Linux thirdparty CI build): reproduces the failure on any Linux host. An `LD_PRELOAD` shim gives libunwind a 64 KiB-page kernel's behaviour (`EINVAL` for unaligned `mincore`/`msync`, `sysconf(_SC_PAGESIZE)=65536`), and a program shaped like the bRPC warm-up calls `backtrace()` from a constructor with a zeroed stack. Against 1.6.2 it segfaults with the customer's exact stack and cursor state; against 1.8.3 it passes. No BE source change: every `unw_*` API the BE, jemalloc and the x86_64 stack-trace code use is unchanged in 1.8.3, `cxx_exceptions` stays off on x86_64/aarch64, and the weak `backtrace` alias is still provided. Follow-ups outside this PR: the doris-website ARM compilation page still says `export USE_UNWIND=OFF` is required, which has been a silent no-op since #64093. ### Release note Fix the aarch64 BE crashing before `main()` (SIGSEGV in `access_mem`/`is_plt_entry` under `unw_backtrace`) on 64 KiB-page kernels such as Kunpeng 920 with Kylin, openEuler or CentOS, by upgrading the bundled libunwind from 1.6.2 to 1.8.3. ### Check List (For Author) - Test - [x] Manual test (add detailed scripts or steps below) - Symbolised the customer core against the matching 4.1.4 arm64 `doris_be` (gdb): crash stack, `si_addr=0`, `PAGESZ=65536`, cursor `ip=0`/`validate=0`, `mem_validate_func=msync_validate`, `last_good_addr={0}`, `uc.regs[30]=0`; `backtrace` == `unw_backtrace` in the 4.1.4 binary vs `backtrace@GLIBC_2.17` in 4.1.3. - Reproduced the identical crash on a 4 KiB arm64 Linux container by running the real 4.1.4 `doris_be --version` under the 64 KiB shim and zeroing `uc.regs[30]` in gdb. - Built libunwind 1.6.2 (+Doris patch) and 1.8.3 (+rebased patch) for aarch64 with the exact recipe from `build_libunwind()`; ran `thirdparty/test/libunwind-page-size-test.sh` against both: 1.6.2 FAIL (SIGSEGV, same stack), 1.8.3 PASS with and without the Doris hook, and with `ip` forced to 0 in gdb. - Same build + test on x86_64 (emulated ubuntu-22.04): PASS. - The rebased patch applies with both GNU `patch` and Apple `patch`; `thirdparty/test/download-thirdparty-*-test.sh`, `juicefs-default-mirror-test.sh`, `azure-vcpkg-retry-test.sh` pass; shellcheck reports no new findings. - Not done here: a Linux `doris_be` rebuilt with 1.8.3 on a real 64 KiB-page host. That is the one check that still needs an aarch64 machine. - Behavior changed: - [x] Yes. Bundled libunwind 1.6.2 → 1.8.3; libunwind's own test programs and man pages are no longer built. Unwinding semantics for the BE otherwise unchanged. - Does this need documentation? - [x] No. ### Check List (For Reviewer who merge this PR) - [ ] Confirm the release note - [ ] Confirm test cases - [ ] Confirm document - [ ] Add branch pick label 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016MdauUJh8AtE8SozZ29Dmv Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
1 parent 6e49d3f commit e797e99

7 files changed

Lines changed: 201 additions & 11 deletions

File tree

‎.github/workflows/build-thirdparty.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ jobs:
196196
#./build-thirdparty.sh -j "$(nproc)"
197197
./build-thirdparty.sh -j 2
198198
199+
# CI has no 64 KiB-page aarch64 host; this drives the libunwind just built
200+
# through an emulated one, so it also runs on this x86_64 4 KiB runner.
201+
cd "${GITHUB_WORKSPACE}"
202+
thirdparty/test/libunwind-page-size-test.sh
203+
199204
build_macos:
200205
name: Build Third Party Libraries (macOS)
201206
needs: changes

‎thirdparty/CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
This file contains version of the third-party dependency libraries in the build-env image. The docker build-env image is apache/doris, and the tag is `build-env-${version}`
44

5+
## 20260911
6+
7+
- Modified: libunwind 1.6.2 -> 1.8.3. 1.6.2 hard-codes a 4 KiB page size in the
8+
AArch64 memory validator (`mincore`/`msync` on 4 KiB-aligned addresses), so on a
9+
64 KiB-page kernel every validation fails, `unw_step` falls back to a link register
10+
that `getcontext_trace` never saved and dereferences it unvalidated. This crashed
11+
every aarch64 BE on such kernels before `main()`, in bRPC's `backtrace()` warm-up.
12+
1.8.x reads the page size at runtime, validates through the pipe-write probe only,
13+
and validates addresses before the DWARF-failure fallback. The Doris PHDR-cache hook
14+
patch is rebased onto 1.8.3; the test programs and man pages are no longer built.
15+
516
## 20260909
617

718
- Modified: snappy 1.1.10 -> 1.2.1. Enable x86 SSE4.2 paths and AVX2 by default;

‎thirdparty/build-thirdparty.sh‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1834,7 +1834,9 @@ build_libunwind() {
18341834
# LIBUNWIND_IS_NATIVE_ONLY: https://lists.llvm.org/pipermail/cfe-commits/Week-of-Mon-20160523/159802.html
18351835
# -nostdinc++ only required for gcc compilation
18361836
cflags="-I${TP_INCLUDE_DIR} -std=c99 -D_LIBUNWIND_NO_HEAP=1 -D_DEBUG -D_LIBUNWIND_IS_NATIVE_ONLY -O3 -fno-exceptions -funwind-tables -fno-sanitize=all -nostdinc++ -fno-rtti -Wno-error=incompatible-pointer-types"
1837-
CFLAGS="${cflags}" LDFLAGS="-L${TP_LIB_DIR} -llzma" ../configure --prefix="${TP_INSTALL_DIR}" --disable-shared --enable-static
1837+
# Only the library is consumed; the test programs and man pages are not.
1838+
CFLAGS="${cflags}" LDFLAGS="-L${TP_LIB_DIR} -llzma" ../configure --prefix="${TP_INSTALL_DIR}" --disable-shared --enable-static \
1839+
--disable-tests --disable-documentation
18381840

18391841
make -j "${PARALLEL}"
18401842
make install

‎thirdparty/download-thirdparty.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,10 +526,10 @@ fi
526526
# patch libunwind so Doris can force GNU libunwind to use the BE PHDR cache
527527
# without changing ordinary dl_iterate_phdr callers.
528528
if [[ " ${TP_ARCHIVES[*]} " =~ " LIBUNWIND " ]]; then
529-
if [[ "${LIBUNWIND_SOURCE}" = "libunwind-1.6.2" ]]; then
529+
if [[ "${LIBUNWIND_SOURCE}" = "libunwind-1.8.3" ]]; then
530530
cd "${TP_SOURCE_DIR}/${LIBUNWIND_SOURCE}"
531531
if [[ ! -f "${PATCHED_MARK}" ]]; then
532-
patch -p1 <"${TP_PATCH_DIR}/libunwind-1.6.2-doris-phdr-cache.patch"
532+
patch -p1 <"${TP_PATCH_DIR}/libunwind-1.8.3-doris-phdr-cache.patch"
533533
touch "${PATCHED_MARK}"
534534
fi
535535
cd -

thirdparty/patches/libunwind-1.6.2-doris-phdr-cache.patch renamed to thirdparty/patches/libunwind-1.8.3-doris-phdr-cache.patch

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
diff --git a/src/dwarf/Gfind_proc_info-lsb.c b/src/dwarf/Gfind_proc_info-lsb.c
2-
index 1d0d6a4..8f47463 100644
2+
index c11345e..837fbfe 100644
33
--- a/src/dwarf/Gfind_proc_info-lsb.c
44
+++ b/src/dwarf/Gfind_proc_info-lsb.c
5-
@@ -47,6 +47,14 @@ struct table_entry
5+
@@ -46,6 +46,14 @@ struct table_entry
66

77
#ifndef UNW_REMOTE_ONLY
88

@@ -21,11 +21,11 @@ index 1d0d6a4..8f47463 100644
2121
cb_data.di_debug.format = -1;
2222

2323
SIGPROCMASK (SIG_SETMASK, &unwi_full_mask, &saved_mask);
24-
- ret = dl_iterate_phdr (dwarf_callback, &cb_data);
24+
- ret = as->iterate_phdr_function (dwarf_callback, &cb_data);
2525
+ if (doris_unwind_iterate_phdr)
2626
+ ret = doris_unwind_iterate_phdr (dwarf_callback, &cb_data, ip);
2727
+ else
28-
+ ret = dl_iterate_phdr (dwarf_callback, &cb_data);
28+
+ ret = as->iterate_phdr_function (dwarf_callback, &cb_data);
2929
SIGPROCMASK (SIG_SETMASK, &saved_mask, NULL);
3030

3131
if (ret > 0)
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env bash
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
# libunwind 1.6.2 hard-coded a 4 KiB page size in its AArch64 memory validator.
20+
# On a 64 KiB-page kernel (Kunpeng/Kylin/openEuler/CentOS aarch64) mincore() and
21+
# msync() reject its 4 KiB-aligned probes with EINVAL, every validation fails,
22+
# unw_step() falls back to a link register that getcontext_trace never saved,
23+
# and is_plt_entry() dereferences that garbage unvalidated. Because the static
24+
# libunwind's weak `backtrace` alias replaces glibc's, bRPC's pre-main
25+
# `backtrace()` warm-up crashed every aarch64 BE on such hosts before main().
26+
#
27+
# CI has no 64 KiB-page host, so this drives the installed libunwind.a through a
28+
# program shaped like that warm-up while an LD_PRELOAD shim makes the kernel
29+
# look like a 64 KiB-page one to libunwind: the same EINVAL for unaligned
30+
# probes and a 64 KiB sysconf(_SC_PAGESIZE). Run it after
31+
# `build-thirdparty.sh libunwind` on Linux; it only needs a C compiler.
32+
33+
set -eo pipefail
34+
35+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)"
36+
TP_INSTALL_DIR="${TP_INSTALL_DIR:-${ROOT}/installed}"
37+
38+
fail() {
39+
echo "FAIL: $*" >&2
40+
exit 1
41+
}
42+
43+
if [[ "$(uname -s)" != 'Linux' ]]; then
44+
echo "SKIP: GNU libunwind is only built on Linux"
45+
exit 0
46+
fi
47+
48+
if [[ -n "${CC:-}" ]]; then
49+
cc="${CC}"
50+
else
51+
for candidate in gcc clang cc; do
52+
if command -v "${candidate}" >/dev/null 2>&1; then
53+
cc="${candidate}"
54+
break
55+
fi
56+
done
57+
fi
58+
[[ -n "${cc:-}" ]] || fail "no C compiler found; set CC"
59+
[[ -f "${TP_INSTALL_DIR}/lib/libunwind.a" ]] || fail "${TP_INSTALL_DIR}/lib/libunwind.a not found; build libunwind first"
60+
61+
tmpdir="$(mktemp -d)"
62+
trap 'rm -rf "${tmpdir}"' EXIT
63+
64+
cat >"${tmpdir}/shim64k.c" <<'EOF'
65+
#define _GNU_SOURCE
66+
#include <dlfcn.h>
67+
#include <errno.h>
68+
#include <stddef.h>
69+
#include <stdint.h>
70+
#include <sys/mman.h>
71+
#include <unistd.h>
72+
73+
/* What a 64 KiB-page kernel does with libunwind's validation probes. */
74+
#define EMU_PAGE 65536UL
75+
76+
int mincore(void *addr, size_t len, unsigned char *vec) {
77+
static int (*real)(void *, size_t, unsigned char *);
78+
if (!real) real = dlsym(RTLD_NEXT, "mincore");
79+
if ((uintptr_t)addr & (EMU_PAGE - 1)) { errno = EINVAL; return -1; }
80+
size_t n = (len + EMU_PAGE - 1) / EMU_PAGE;
81+
for (size_t i = 0; i < n; i++) {
82+
unsigned char tmp[EMU_PAGE / 4096];
83+
if (real((char *)addr + i * EMU_PAGE, EMU_PAGE, tmp) != 0) return -1;
84+
unsigned char any = 0;
85+
for (size_t j = 0; j < sizeof tmp; j++) any |= tmp[j] & 1;
86+
vec[i] = any;
87+
}
88+
return 0;
89+
}
90+
int msync(void *addr, size_t len, int flags) {
91+
static int (*real)(void *, size_t, int);
92+
if (!real) real = dlsym(RTLD_NEXT, "msync");
93+
if ((uintptr_t)addr & (EMU_PAGE - 1)) { errno = EINVAL; return -1; }
94+
return real(addr, len, flags);
95+
}
96+
long sysconf(int name) {
97+
static long (*real)(int);
98+
if (!real) real = dlsym(RTLD_NEXT, "sysconf");
99+
if (name == _SC_PAGESIZE) return EMU_PAGE;
100+
return real(name);
101+
}
102+
int getpagesize(void) { return (int)EMU_PAGE; }
103+
EOF
104+
105+
cat >"${tmpdir}/premain.c" <<'EOF'
106+
#define _GNU_SOURCE
107+
#include <execinfo.h>
108+
#include <link.h>
109+
#include <stdio.h>
110+
#include <string.h>
111+
#include <unistd.h>
112+
113+
/* be/src/common/phdr_cache.cpp before main(): the PHDR cache is still NULL, so the
114+
* Doris hook that the libunwind patch routes dwarf_find_proc_info() through finds
115+
* nothing. */
116+
int doris_unwind_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data,
117+
unsigned long ip) {
118+
(void)callback; (void)data; (void)ip;
119+
return 0;
120+
}
121+
122+
static void *dummy_buf[4];
123+
static int dummy_bt = -1;
124+
125+
/* Zero the stack below us, like the never-touched region the warm-up ran on, so the
126+
* uc.regs[30] slot that _Uaarch64_getcontext_trace never writes reads as 0. */
127+
static void __attribute__((noinline)) scrub_stack(void) {
128+
volatile char buf[256 * 1024];
129+
memset((char *)buf, 0, sizeof(buf));
130+
__asm__ volatile("" ::: "memory");
131+
}
132+
133+
/* bRPC src/bthread/mutex.cpp: "Warm up backtrace before main()." */
134+
__attribute__((constructor)) static void warm_up(void) {
135+
scrub_stack();
136+
dummy_bt = backtrace(dummy_buf, 4);
137+
}
138+
139+
int main(void) {
140+
void *buf[32];
141+
int n = backtrace(buf, 32);
142+
printf("premain=%d runtime=%d pagesz=%ld\n", dummy_bt, n, sysconf(_SC_PAGESIZE));
143+
return (dummy_bt < 0 || n < 0) ? 1 : 0;
144+
}
145+
EOF
146+
147+
extra_libs=()
148+
[[ -f "${TP_INSTALL_DIR}/lib/libz.a" ]] && extra_libs+=(-lz)
149+
150+
"${cc}" -O2 -g -shared -fPIC -o "${tmpdir}/shim64k.so" "${tmpdir}/shim64k.c" -ldl
151+
# -u unw_backtrace pulls libunwind's backtrace.o in, whose weak `backtrace` alias then
152+
# takes over the call, exactly as it does in doris_be.
153+
"${cc}" -O2 -g -fno-omit-frame-pointer -o "${tmpdir}/premain" "${tmpdir}/premain.c" \
154+
-I"${TP_INSTALL_DIR}/include" -L"${TP_INSTALL_DIR}/lib" \
155+
-Wl,-u,unw_backtrace -lunwind -llzma "${extra_libs[@]}" -lpthread
156+
157+
if command -v nm >/dev/null 2>&1; then
158+
nm "${tmpdir}/premain" | grep -Eq ' [WT] backtrace$' ||
159+
fail "backtrace() is not bound to libunwind's unw_backtrace in the test program"
160+
fi
161+
162+
echo "== native page size =="
163+
out="$("${tmpdir}/premain")" || fail "pre-main backtrace() crashed on the native page size: ${out}"
164+
echo "${out}"
165+
166+
echo "== emulated 64 KiB page size =="
167+
out="$(LD_PRELOAD="${tmpdir}/shim64k.so" "${tmpdir}/premain")" ||
168+
fail "pre-main backtrace() crashed on an emulated 64 KiB-page kernel: ${out}"
169+
echo "${out}"
170+
[[ "${out}" == *"pagesz=65536"* ]] || fail "the 64 KiB page emulation did not take effect: ${out}"
171+
172+
echo "PASS"

‎thirdparty/vars.sh‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,10 @@ JEMALLOC_DORIS_SOURCE="jemalloc-5.3.0"
353353
JEMALLOC_DORIS_MD5SUM="09a8328574dab22a7df848eae6dbbf53"
354354

355355
# libunwind
356-
LIBUNWIND_DOWNLOAD="https://github.com/libunwind/libunwind/releases/download/v1.6.2/libunwind-1.6.2.tar.gz"
357-
LIBUNWIND_NAME="libunwind-1.6.2.tar.gz"
358-
LIBUNWIND_SOURCE="libunwind-1.6.2"
359-
LIBUNWIND_MD5SUM="f625b6a98ac1976116c71708a73dc44a"
356+
LIBUNWIND_DOWNLOAD="https://github.com/libunwind/libunwind/releases/download/v1.8.3/libunwind-1.8.3.tar.gz"
357+
LIBUNWIND_NAME="libunwind-1.8.3.tar.gz"
358+
LIBUNWIND_SOURCE="libunwind-1.8.3"
359+
LIBUNWIND_MD5SUM="13bc7b41462ac6ea157d350eaf6c1503"
360360

361361
# cctz
362362
CCTZ_DOWNLOAD="https://github.com/google/cctz/archive/refs/tags/v2.5.tar.gz"

0 commit comments

Comments
 (0)