Skip to content

[multi_isa][GCC] SVE Iterator COMDAT can cause SIGILL on non-SVE AArch64 #1318

Description

@allnes

Output of strings libarm_compute-static.a | grep arm_compute_version:

arm_compute_version=v0.0-unreleased Build options: {'neon': '1', 'opencl': '0', 'examples': '0', 'Werror': '0', 'gemm_tuner': '0', 'reference_openmp': '0', 'validation_tests': '0', 'benchmark_tests': '0', 'data_layout_support': 'all', 'arch': 'arm64-v8a', 'build_dir': 'arm64-v8a', 'openmp': '0', 'cppthreads': '1', 'estate': '64', 'multi_isa': '1', 'os': 'linux', 'build': 'cross_compile', 'toolchain_prefix': 'aarch64-linux-gnu-', 'fixed_format_kernels': 'True'} Git hash=b'5ef9d31bd7c63b4a096d30a362bd1111ae426f51'

Platform: Raspberry Pi 4, Cortex-A72, AArch64; CPU features are fp asimd evtstrm crc32 cpuid (no SVE)

Operating System: Debian/Raspberry Pi OS, kernel 6.12.75+rpt-rpi-v8

Problem description

A GCC-built multi_isa=1 static library can export an SVE-autovectorized weak/COMDAT definition of the common inline arm_compute::Iterator constructor. Depending on normal static-link extraction order, that definition can replace the baseline Armv8-A copy used by CpuCastKernel. Running a public NECast operation on a non-SVE CPU then executes mov z0.b, #0 and terminates with SIGILL.

This reproduces from clean ACL main commit 5ef9d31bd7c63b4a096d30a362bd1111ae426f51, without OpenVINO, LTO, internal ACL symbols, --undefined, or --whole-archive.

Reproducer

activation_component.cpp:

#include "arm_compute/core/TensorInfo.h"
#include "arm_compute/function_info/ActivationLayerInfo.h"
#include "arm_compute/runtime/NEON/functions/NEActivationLayer.h"
#include "arm_compute/runtime/Tensor.h"

int run_activation_component()
{
    using namespace arm_compute;
    Tensor input, output;
    input.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::F32));
    output.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::F32));
    NEActivationLayer activation;
    activation.configure(&input, &output,
                         ActivationLayerInfo(ActivationLayerInfo::ActivationFunction::RELU));
    input.allocator()->allocate();
    output.allocator()->allocate();
    activation.run();
    return 0;
}

cast_main.cpp:

#include "arm_compute/core/TensorInfo.h"
#include "arm_compute/runtime/NEON/functions/NECast.h"
#include "arm_compute/runtime/Tensor.h"
#include <cstdint>
#include <iostream>

int run_activation_component();

int main()
{
    using namespace arm_compute;
    Tensor input, output;
    input.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::S16));
    output.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::S32));
    NECast cast;
    cast.configure(&input, &output, ConvertPolicy::SATURATE);
    input.allocator()->allocate();
    output.allocator()->allocate();
    auto *src = reinterpret_cast<int16_t *>(input.buffer());
    for (int i = 0; i < 64; ++i) src[i] = static_cast<int16_t>(i - 32);
    std::cout << "before_cast" << std::endl;
    cast.run();
    std::cout << "after_cast" << std::endl;
    return run_activation_component();
}

Cross-build with GCC 10.5.0:

git clone https://github.com/ARM-software/ComputeLibrary.git
cd ComputeLibrary
git checkout 5ef9d31bd7c63b4a096d30a362bd1111ae426f51

scons -j8 neon=1 opencl=0 examples=0 Werror=0 gemm_tuner=0 \
  reference_openmp=0 validation_tests=0 benchmark_tests=0 \
  data_layout_support=all arch=arm64-v8a build_dir=arm64-v8a \
  openmp=0 cppthreads=1 estate=64 multi_isa=1 \
  os=linux build=cross_compile toolchain_prefix=aarch64-linux-gnu- \
  fixed_format_kernels=True build/arm64-v8a/libarm_compute-static.a

CXX=aarch64-linux-gnu-g++
LIB="$PWD/build/arm64-v8a/libarm_compute-static.a"
$CXX -std=c++17 -O2 -g -march=armv8-a -I. -Iinclude \
  -c ../activation_component.cpp -o activation_component.o
$CXX -std=c++17 -O2 -g -march=armv8-a -I. -Iinclude \
  -c ../cast_main.cpp -o cast_main.o

# Failing order. The archive is repeated normally; no member is forced in.
$CXX -Wl,-Map,acl-public-reproducer.map \
  activation_component.o "$LIB" cast_main.o "$LIB" \
  -pthread -ldl -o acl-public-reproducer

# A/B control: identical inputs, only component order is reversed.
$CXX -Wl,-Map,acl-public-control.map \
  cast_main.o "$LIB" activation_component.o "$LIB" \
  -pthread -ldl -o acl-public-control

Copy both executables to a non-SVE AArch64 system and run them.

Actual result

The reproducer failed in 5/5 fresh processes; the control passed 3/3:

run=1 rc=132 output=before_cast
run=2 rc=132 output=before_cast
run=3 rc=132 output=before_cast
run=4 rc=132 output=before_cast
run=5 rc=132 output=before_cast
control_run=1 rc=0 output=before_cast after_cast
control_run=2 rc=0 output=before_cast after_cast
control_run=3 rc=0 output=before_cast after_cast

The tested SHA-256 values were 53ce7ad058fcc89d9230d3b21a577d69f9833ea60ffba45ca73ba8685010b03f for libarm_compute-static.a, 2c5d3c83393cbe421310177afc2e7f4fcdcafbbcf16fd7ccdae3580eeaaf0b1a for the failing executable, and 221348c3c75b3251af245a2ac27da04f0e99380c750c46e554ad56282f7bfefc for the control.

GDB shows the unguarded SVE instruction and baseline call path:

Thread 1 received signal SIGILL, Illegal instruction.
arm_compute::Iterator::Iterator(...):
=> mov z0.b, #0

#0 arm_compute::Iterator::Iterator(...)
#1 arm_compute::cpu::kernels::CpuCastKernel::run_op(...)
#2 arm_compute::IScheduler::schedule_common(...)
#3 arm_compute::experimental::INEOperator::run(...)
#4 arm_compute::NECast::run()
#5 main() at cast_main.cpp:33

The failing link map selects the constructor from the SVE activation object:

.text._ZN11arm_compute8IteratorC2EPKNS_7ITensorERKNS_6WindowE
  0x00000000000337f0 0x214 libarm_compute-static.a(fp32.o)
  arm_compute::Iterator::Iterator(...)

The preceding map symbol identifies that member as src/cpu/kernels/activation/generic/sve/fp32.cpp. Its constructor starts with:

337fc: mov     z0.b, #0
33810: whilelo p0.d, xzr, x2
33830: st1d    {z0.d}, p0, [x4, x3, lsl #3]

In the passing control, the retained constructor instead comes from CpuCastKernel.o and contains only baseline AArch64 instructions.

Expected result

A library built with multi_isa=1 arch=arm64-v8a must not execute SVE instructions before runtime ISA dispatch on an Armv8-A/ASIMD-only CPU. Both link orders should complete successfully.

Root cause

Iterator is inline and therefore emitted by both baseline and ISA-specific translation units. GCC 10 auto-vectorizes its _dims() initialization with SVE when emitted from activation/generic/sve/fp32.cpp. The linker coalesces these same-named weak/COMDAT definitions and may retain the SVE body for baseline callers.

For reference, the monolithic arm_compute_validation Cast test passed 3/3 on the same target because its link layout retained the baseline constructor. That does not cover this link-order-dependent case.

Proposed fix

Pull request #1317

Fix validation

The patched tree was rebuilt from scratch with the same GCC 10.5.0 command.
The exact formerly failing executable was run on the same non-SVE Cortex-A72
target:

run=1 rc=0 output=before_cast after_cast
run=2 rc=0 output=before_cast after_cast
run=3 rc=0 output=before_cast after_cast
run=4 rc=0 output=before_cast after_cast
run=5 rc=0 output=before_cast after_cast
control_run=1 rc=0 output=before_cast after_cast
control_run=2 rc=0 output=before_cast after_cast
control_run=3 rc=0 output=before_cast after_cast

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions