Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f64dc6d
feat(libunwind): build vendored libunwind for 32-bit ARM
jpnurmi Apr 20, 2026
db28bf4
Update CHANGELOG.md
jpnurmi Apr 20, 2026
273f01f
run arm32 tests under qemu-user
jpnurmi Apr 20, 2026
f898755
Skip tests that don't work under arm32 cross-compile + qemu-user
jpnurmi Apr 20, 2026
a30e9ef
Rename arm32 CI row to reflect cross-toolchain + qemu-user
jpnurmi Apr 20, 2026
932104d
Cast is_arm32 / is_qemu to bool for pytest skipif
jpnurmi Apr 20, 2026
9ddf48e
Set SENTRY_BACKEND=none in embedded-info binary tests
jpnurmi Apr 20, 2026
69ce76e
Bump external/crashpad (getsentry/crashpad#149)
jpnurmi Apr 21, 2026
c5830d1
Enable crashpad integration tests on arm32
jpnurmi Apr 21, 2026
ede2025
Remove over-broad is_qemu skipifs in test_integration_crashpad.py
jpnurmi Apr 21, 2026
4dbdbdf
feat(native): write thread context for 32-bit ARM
jpnurmi Apr 21, 2026
5f8f382
Enable native integration tests on arm32
jpnurmi Apr 21, 2026
b914b04
Gate has_native on qemu
jpnurmi Apr 21, 2026
5e73464
Skip native crash tests on qemu, keep build test running
jpnurmi Apr 21, 2026
bff3713
Update CHANGELOG.md
jpnurmi Apr 21, 2026
1c408c6
fix(native): initialize ptrace_sp / sp on 32-bit ARM
jpnurmi Apr 21, 2026
aeb2d94
feat(native): ptrace-capture registers for non-crashed threads on 32-…
jpnurmi Apr 21, 2026
a3ecf71
fix(native): pad minidump_context_arm_t to match on-disk minidump spec
jpnurmi Apr 21, 2026
30dd71b
fix(native): use breakpad's 0x40000000 MD_CONTEXT_ARM flag
jpnurmi Apr 21, 2026
35f6286
Drop verbose VFP comment from minidump_context_arm_t
jpnurmi Apr 21, 2026
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
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ jobs:
CC: gcc-9
CXX: g++-9
TEST_X86: 1
- name: Linux Arm32 (gcc-arm-linux-gnueabihf + qemu-user)
os: ubuntu-24.04-arm
CC: arm-linux-gnueabihf-gcc
CXX: arm-linux-gnueabihf-g++
TEST_ARM32: 1
TEST_QEMU: 1
- name: Linux (GCC 9.5.0)
os: ubuntu-24.04
CC: gcc-9
Expand Down Expand Up @@ -223,6 +229,8 @@ jobs:
CXX: ${{ matrix.CXX }}
TEST_X86: ${{ matrix.TEST_X86 }}
TEST_MINGW: ${{ matrix.TEST_MINGW }}
TEST_ARM32: ${{ matrix.TEST_ARM32 }}
TEST_QEMU: ${{ matrix.TEST_QEMU }}
USE_SCCACHE: ${{ matrix.USE_SCCACHE }}
SCCACHE_GHA_ENABLED: ${{ matrix.USE_SCCACHE && 'true' || '' }}
ERROR_ON_WARNINGS: ${{ matrix.ERROR_ON_WARNINGS }}
Expand Down Expand Up @@ -258,7 +266,7 @@ jobs:
[ -n "$CC" ] && [ -n "$CXX" ] || { echo "Ubuntu runner configurations require toolchain selection via CC and CXX" >&2; exit 1; }

- name: Installing Linux Dependencies
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !env['ANDROID_API'] && !matrix.container }}
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !env['TEST_ARM32'] && !env['ANDROID_API'] && !matrix.container }}
run: |
sudo apt update
# Install common dependencies
Expand Down Expand Up @@ -295,6 +303,26 @@ jobs:
sudo apt update
sudo apt install cmake "${CC}-multilib" "${CXX}-multilib" zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 liblzma-dev:i386

- name: Installing Linux arm32 Dependencies
if: ${{ runner.os == 'Linux' && env['TEST_ARM32'] && !matrix.container }}
run: |
sudo dpkg --add-architecture armhf
sudo apt update
sudo apt install -y \
cmake \
gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf \
zlib1g-dev:armhf \
libssl-dev:armhf \
libcurl4-openssl-dev:armhf \
liblzma-dev:armhf \
libstdc++6:armhf

- name: Installing qemu-user
if: ${{ runner.os == 'Linux' && env['TEST_QEMU'] && !matrix.container }}
run: |
sudo apt update
sudo apt install -y qemu-user-static binfmt-support

- name: Installing Alpine Linux Dependencies
if: ${{ contains(matrix.container, 'alpine') }}
run: |
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

## Unreleased

**Features**:

- Linux: support 32-bit ARM. ([#1659](https://github.com/getsentry/sentry-native/issues/1659))

**Fixes**:

- Linux: handle `ENOSYS` in `read_safely` to fix empty module list in seccomp-restricted environments. ([#1655](https://github.com/getsentry/sentry-native/pull/1655))
- macOS: avoid stdio deadlock in breakpad exception handler. ([#1656](https://github.com/getsentry/sentry-native/pull/1656))
- Crashpad: build for 32-bit ARM on Linux. ([#1659](https://github.com/getsentry/sentry-native/issues/1659))
- Native: build for 32-bit ARM on Linux. ([#1659](https://github.com/getsentry/sentry-native/issues/1659))
- Inproc: build vendored libunwind for 32-bit ARM on Linux. ([#1659](https://github.com/getsentry/sentry-native/issues/1659))

## 0.13.7

Expand Down
13 changes: 10 additions & 3 deletions src/backends/native/minidump/sentry_minidump_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,17 @@ PACKED_STRUCT_BEGIN
typedef struct {
uint32_t context_flags;
uint32_t r[13]; // R0-R12
uint32_t sp;
uint32_t lr;
uint32_t pc;
uint32_t sp; // R13
uint32_t lr; // R14
uint32_t pc; // R15
uint32_t cpsr;
// VFP/NEON state (matches breakpad's MDRawContextARM / Microsoft's
// CONTEXT_ARM floating-point area). We don't populate these today
// but the layout needs to match what downstream minidump parsers
// (crashpad, rust-minidump, breakpad) expect when reading the stream.
uint64_t fpscr;
Comment thread
cursor[bot] marked this conversation as resolved.
uint64_t fpregs[32]; // D0-D31
uint32_t fpextra[8];
} PACKED_ATTR minidump_context_arm_t;
PACKED_STRUCT_END
#endif
Expand Down
60 changes: 60 additions & 0 deletions src/backends/native/minidump/sentry_minidump_linux.c
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,34 @@ ptrace_get_thread_registers(pid_t tid, ucontext_t *uctx)
SENTRY_DEBUGF("ptrace(PTRACE_GETREGS) failed for thread %d: %s", tid,
strerror(errno));
}
# elif defined(__arm__)
struct user_regs regs;
if (ptrace(PTRACE_GETREGS, tid, NULL, &regs) == 0) {
// uregs[0..15] = R0..R15, uregs[16] = CPSR
uctx->uc_mcontext.arm_r0 = regs.uregs[0];
uctx->uc_mcontext.arm_r1 = regs.uregs[1];
uctx->uc_mcontext.arm_r2 = regs.uregs[2];
uctx->uc_mcontext.arm_r3 = regs.uregs[3];
uctx->uc_mcontext.arm_r4 = regs.uregs[4];
uctx->uc_mcontext.arm_r5 = regs.uregs[5];
uctx->uc_mcontext.arm_r6 = regs.uregs[6];
uctx->uc_mcontext.arm_r7 = regs.uregs[7];
uctx->uc_mcontext.arm_r8 = regs.uregs[8];
uctx->uc_mcontext.arm_r9 = regs.uregs[9];
uctx->uc_mcontext.arm_r10 = regs.uregs[10];
uctx->uc_mcontext.arm_fp = regs.uregs[11];
uctx->uc_mcontext.arm_ip = regs.uregs[12];
uctx->uc_mcontext.arm_sp = regs.uregs[13];
uctx->uc_mcontext.arm_lr = regs.uregs[14];
uctx->uc_mcontext.arm_pc = regs.uregs[15];
uctx->uc_mcontext.arm_cpsr = regs.uregs[16];
success = true;
SENTRY_DEBUGF("Thread %d: captured registers via ptrace, SP=0x%lx", tid,
(unsigned long)regs.uregs[13]);
} else {
SENTRY_DEBUGF("ptrace(PTRACE_GETREGS) failed for thread %d: %s", tid,
strerror(errno));
}
# endif

// Detach from thread
Expand Down Expand Up @@ -749,6 +777,34 @@ write_thread_context(

return write_data(writer, &context, sizeof(context));

# elif defined(__arm__)
(void)tid; // Unused on ARM32 - no VFP capture implemented yet

minidump_context_arm_t context = { 0 };
// CONTEXT_ARM | CONTEXT_CONTROL | CONTEXT_INTEGER
context.context_flags = 0x00200003;
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated

// Copy general purpose registers R0-R10 from Linux ucontext
context.r[0] = uctx->uc_mcontext.arm_r0;
context.r[1] = uctx->uc_mcontext.arm_r1;
context.r[2] = uctx->uc_mcontext.arm_r2;
context.r[3] = uctx->uc_mcontext.arm_r3;
context.r[4] = uctx->uc_mcontext.arm_r4;
context.r[5] = uctx->uc_mcontext.arm_r5;
context.r[6] = uctx->uc_mcontext.arm_r6;
context.r[7] = uctx->uc_mcontext.arm_r7;
context.r[8] = uctx->uc_mcontext.arm_r8;
context.r[9] = uctx->uc_mcontext.arm_r9;
context.r[10] = uctx->uc_mcontext.arm_r10;
context.r[11] = uctx->uc_mcontext.arm_fp; // R11 (FP)
context.r[12] = uctx->uc_mcontext.arm_ip; // R12 (IP)
context.sp = uctx->uc_mcontext.arm_sp;
context.lr = uctx->uc_mcontext.arm_lr;
context.pc = uctx->uc_mcontext.arm_pc;
context.cpsr = uctx->uc_mcontext.arm_cpsr;

return write_data(writer, &context, sizeof(context));
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

# else
# error "Unsupported architecture for Linux"
# endif
Expand Down Expand Up @@ -1285,6 +1341,8 @@ ptrace_capture_thread(
ptrace_sp = ptrace_ctx.uc_mcontext.sp;
# elif defined(__i386__)
ptrace_sp = ptrace_ctx.uc_mcontext.gregs[REG_ESP];
# elif defined(__arm__)
ptrace_sp = ptrace_ctx.uc_mcontext.arm_sp;
# endif

if (ptrace_sp != 0) {
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -1369,6 +1427,8 @@ write_thread_list_stream(minidump_writer_t *writer, minidump_directory_t *dir)
sp = uctx->uc_mcontext.sp;
# elif defined(__i386__)
sp = uctx->uc_mcontext.gregs[REG_ESP];
# elif defined(__arm__)
sp = uctx->uc_mcontext.arm_sp;
# endif

SENTRY_DEBUGF("Thread %u: has context, SP=0x%llx",
Expand Down
10 changes: 10 additions & 0 deletions tests/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ def get_platform_cmake_args():
args.append("-AWin32")
elif sys.platform == "linux" and os.environ.get("TEST_X86"):
args.append("-DSENTRY_BUILD_FORCE32=ON")
elif sys.platform == "linux" and os.environ.get("TEST_ARM32"):
args.extend(
[
"-DCMAKE_SYSTEM_NAME=Linux",
"-DCMAKE_SYSTEM_PROCESSOR=arm",
"-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc",
"-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++",
"-DCMAKE_ASM_COMPILER=arm-linux-gnueabihf-gcc",
]
)

if "asan" in os.environ.get("RUN_ANALYZER", ""):
args.append("-DWITH_ASAN_OPTION=ON")
Expand Down
2 changes: 2 additions & 0 deletions tests/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
is_aix = sys.platform == "aix" or sys.platform == "os400"
is_android = os.environ.get("ANDROID_API")
is_x86 = os.environ.get("TEST_X86")
is_arm32 = bool(os.environ.get("TEST_ARM32"))
is_qemu = bool(os.environ.get("TEST_QEMU"))
is_asan = "asan" in os.environ.get("RUN_ANALYZER", "")
is_tsan = "tsan" in os.environ.get("RUN_ANALYZER", "")
is_kcov = "kcov" in os.environ.get("RUN_ANALYZER", "")
Expand Down
9 changes: 4 additions & 5 deletions tests/test_build_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
import os
import pytest
from .conditions import has_breakpad, has_crashpad, has_native, is_android
from .conditions import has_breakpad, has_crashpad, has_native, is_android, is_qemu


def test_static_lib(cmake):
Expand All @@ -16,7 +16,7 @@ def test_static_lib(cmake):
)

# on linux we can use `ldd` to check that we don’t link to `libsentry.so`
if sys.platform == "linux" and not is_android:
if sys.platform == "linux" and not is_android and not is_qemu:
output = subprocess.check_output("ldd sentry_example", cwd=tmp_path, shell=True)
assert b"libsentry.so" not in output

Expand All @@ -40,9 +40,8 @@ def test_static_lib(cmake):
output = subprocess.check_output(
"file sentry_example", cwd=tmp_path, shell=True
)
assert (
b"ELF 32-bit" if os.environ.get("TEST_X86") else b"ELF 64-bit"
) in output
is_32bit = os.environ.get("TEST_X86") or os.environ.get("TEST_ARM32")
assert (b"ELF 32-bit" if is_32bit else b"ELF 64-bit") in output


@pytest.mark.skipif(not has_crashpad, reason="test needs crashpad backend")
Expand Down
20 changes: 17 additions & 3 deletions tests/test_dotnet_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest

from tests import adb
from tests.conditions import is_android, is_tsan, is_x86, is_asan
from tests.conditions import is_android, is_arm32, is_tsan, is_x86, is_asan

project_fixture_path = pathlib.Path("tests/fixtures/dotnet_signal")

Expand Down Expand Up @@ -67,7 +67,14 @@ def run_dotnet_native_crash(tmp_path):


@pytest.mark.skipif(
bool(sys.platform != "linux" or is_x86 or is_asan or is_tsan or is_android),
bool(
sys.platform != "linux"
or is_x86
or is_arm32
or is_asan
or is_tsan
or is_android
),
reason="dotnet signal handling is currently only supported on 64-bit Linux without sanitizers",
)
def test_dotnet_signals_inproc(cmake):
Expand Down Expand Up @@ -171,7 +178,14 @@ def run_aot_native_crash(tmp_path):


@pytest.mark.skipif(
bool(sys.platform != "linux" or is_x86 or is_asan or is_tsan or is_android),
bool(
sys.platform != "linux"
or is_x86
or is_arm32
or is_asan
or is_tsan
or is_android
),
reason="dotnet AOT signal handling is currently only supported on 64-bit Linux without sanitizers",
)
def test_aot_signals_inproc(cmake):
Expand Down
3 changes: 3 additions & 0 deletions tests/test_embedded_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def test_embedded_info_binary_inspection(cmake):
cwd = cmake(
["sentry"], # Build the library itself
{
"SENTRY_BACKEND": "none",
"SENTRY_EMBED_INFO": "ON",
"SENTRY_BUILD_PLATFORM": "binary-test",
"SENTRY_BUILD_VARIANT": "inspection",
Expand Down Expand Up @@ -157,6 +158,7 @@ def test_sdk_version_override(cmake):
cwd = cmake(
["sentry"],
{
"SENTRY_BACKEND": "none",
"SENTRY_EMBED_INFO": "ON",
"SENTRY_BUILD_PLATFORM": "version-test",
"SENTRY_BUILD_VARIANT": "override",
Expand Down Expand Up @@ -204,6 +206,7 @@ def test_sdk_version_override_with_explicit_build_id(cmake):
cwd = cmake(
["sentry"],
{
"SENTRY_BACKEND": "none",
"SENTRY_EMBED_INFO": "ON",
"SENTRY_BUILD_PLATFORM": "version-test",
"SENTRY_BUILD_VARIANT": "explicit-build",
Expand Down
12 changes: 6 additions & 6 deletions tests/test_integration_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from . import run
from .conditions import has_breakpad, has_files, has_http
from .conditions import has_breakpad, has_files, has_http, is_qemu

pytestmark = [
pytest.mark.skipif(not has_files, reason="tests need local filesystem"),
Expand All @@ -19,7 +19,7 @@
pytest.param(
"breakpad",
marks=pytest.mark.skipif(
not has_breakpad, reason="breakpad backend not available"
not has_breakpad or is_qemu, reason="breakpad backend not available"
),
),
],
Expand Down Expand Up @@ -67,7 +67,7 @@ def test_cache_keep(cmake, backend, cache_keep, unreachable_dsn):
pytest.param(
"breakpad",
marks=pytest.mark.skipif(
not has_breakpad, reason="breakpad backend not available"
not has_breakpad or is_qemu, reason="breakpad backend not available"
),
),
],
Expand Down Expand Up @@ -121,7 +121,7 @@ def test_cache_max_size(cmake, backend, unreachable_dsn):
pytest.param(
"breakpad",
marks=pytest.mark.skipif(
not has_breakpad, reason="breakpad backend not available"
not has_breakpad or is_qemu, reason="breakpad backend not available"
),
),
],
Expand Down Expand Up @@ -176,7 +176,7 @@ def test_cache_max_age(cmake, backend, unreachable_dsn):
pytest.param(
"breakpad",
marks=pytest.mark.skipif(
not has_breakpad, reason="breakpad backend not available"
not has_breakpad or is_qemu, reason="breakpad backend not available"
),
),
],
Expand Down Expand Up @@ -216,7 +216,7 @@ def test_cache_max_items(cmake, backend, unreachable_dsn):
pytest.param(
"breakpad",
marks=pytest.mark.skipif(
not has_breakpad, reason="breakpad backend not available"
not has_breakpad or is_qemu, reason="breakpad backend not available"
),
),
],
Expand Down
Loading
Loading