build(tri-pi): add aarch64 + armhf cross toolchains, QEMU test harness, ARM64 libtor build
Five files for tri-pi cross-compilation and ARM64 Tor support: - cmake/aarch64-toolchain.cmake: CMake toolchain file for aarch64-linux-gnu - cmake/armhf-toolchain.cmake: same, for arm-linux-gnueabihf - scripts/tri-pi-test.sh: QEMU-based test harness (user-mode + full-system) for Pi 3B/3A+/4B/5. Runs the cross-compiled trianglesd under qemu-aarch64-static so ARM binary correctness can be validated without physical Pi hardware. - src/tor/build-libtor-aarch64.sh: cross-compile libtor.a for aarch64 using the vendored configure flow from src/tor/configure.vendored. These accompany the in-progress tri-pi bootstrap work (Hetzner Pi, raspbian packaging). Also staged (separately from the build scripts above): - src/test/fuzz/transaction_deserialize_fuzz.cpp: libFuzzer harness for CTransaction deserialization. Reads raw attacker-controlled bytes into a CDataStream and calls Unserialize on a CTransaction, then exercises hash determinism, round-trip serialize/parse, and CheckTransaction bounds. Mirrors the Bitcoin Core deserialize-fuzz pattern. Not yet wired into src/CMakeLists.txt — the BUILD_FUZZ=ON gate currently only builds fuzz_script; a follow-up patch should add the analogous stanza for this target.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# CMake toolchain for cross-compiling to aarch64 (Pi 3/4/5)
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
|
||||
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
|
||||
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
|
||||
# Also search the multiarch lib path
|
||||
set(CMAKE_LIBRARY_PATH /usr/lib/aarch64-linux-gnu)
|
||||
set(CMAKE_INCLUDE_PATH /usr/include)
|
||||
@@ -0,0 +1,15 @@
|
||||
# CMake toolchain for cross-compiling to armhf (Pi Zero/1/2/3 in 32-bit mode)
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR arm)
|
||||
|
||||
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
|
||||
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
|
||||
set(CMAKE_LIBRARY_PATH /usr/lib/arm-linux-gnueabihf)
|
||||
set(CMAKE_INCLUDE_PATH /usr/include)
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# tri-pi-test.sh — Run Triangles on emulated Raspberry Pi variants via QEMU
|
||||
#
|
||||
# Usage:
|
||||
# ./tri-pi-test.sh [pi-model] [tri-args...]
|
||||
#
|
||||
# Pi models supported (aarch64):
|
||||
# pi3 Pi 3B/3A+ (Cortex-A53, 64-bit) — user-mode QEMU
|
||||
# pi4 Pi 4B (Cortex-A72, 64-bit) — user-mode QEMU
|
||||
# pi5 Pi 5 (Cortex-A76, 64-bit) — user-mode QEMU
|
||||
# pi3-full Pi 3B — full system emulation (qemu-system-aarch64 -M raspi3b)
|
||||
#
|
||||
# Examples:
|
||||
# ./tri-pi-test.sh pi3 --version
|
||||
# ./tri-pi-test.sh pi4 -regtest -notor -recovery-mode=1 -printtoconsole
|
||||
# ./tri-pi-test.sh pi3-full # boots a full Pi OS (needs rootfs image)
|
||||
#
|
||||
# The aarch64 tri binaries are cross-compiled on DNS2 and run under
|
||||
# qemu-aarch64-static. This tests the ARM binary's correctness — ABI
|
||||
# compatibility, library resolution, crypto operations, database access,
|
||||
# and Tor integration — without needing physical Pi hardware.
|
||||
#
|
||||
# For full-system emulation (testing kernel/hardware/driver interaction),
|
||||
# use pi3-full mode with a Raspberry Pi OS rootfs.
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PI_MODEL="${1:-pi3}"
|
||||
shift || true
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TRI_SRC="/root/triangles_v5"
|
||||
TRI_AARCH64_BIN="${TRI_SRC}/build-aarch64/bin/trianglesd"
|
||||
TRI_AARCH64_CLI="${TRI_SRC}/build-aarch64/bin/triangles-cli"
|
||||
QEMU_USER="/usr/bin/qemu-aarch64-static"
|
||||
QEMU_SYS="/usr/bin/qemu-system-aarch64"
|
||||
ARM_SYSROOT="/usr/aarch64-linux-gnu"
|
||||
|
||||
# Verify binary exists
|
||||
if [[ ! -f "$TRI_AARCH64_BIN" ]]; then
|
||||
echo "ERROR: aarch64 trianglesd not found at $TRI_AARCH64_BIN" >&2
|
||||
echo "Build it with: cd $TRI_SRC && cmake --build build-aarch64 --target trianglesd" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_user_mode() {
|
||||
local binary="$1"
|
||||
shift
|
||||
local model_name="$1"
|
||||
shift
|
||||
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Triangles on Raspberry Pi ${model_name} (QEMU user-mode) ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Binary: $(file "$binary" | cut -d: -f2)"
|
||||
echo "QEMU: $($QEMU_USER --version | head -1)"
|
||||
echo "Args: $*"
|
||||
echo ""
|
||||
|
||||
# QEMU user-mode runs the ARM binary with the host kernel but ARM user-space
|
||||
# -L sets the sysroot for dynamic linker/library resolution
|
||||
exec "$QEMU_USER" -L "$ARM_SYSROOT" "$binary" "$@"
|
||||
}
|
||||
|
||||
run_full_system_pi3() {
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Triangles on Raspberry Pi 3B (QEMU full-system) ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
|
||||
local IMG_DIR="${TRI_SRC}/pi-emulation/images"
|
||||
local KERNEL="${IMG_DIR}/kernel8.img"
|
||||
local DTB="${IMG_DIR}/bcm2710-rpi-3-b.dtb"
|
||||
local ROOTFS="${IMG_DIR}/raspios-trixie-arm64.img"
|
||||
local OVERLAY="/tmp/tri-pi3-overlay.qcow2"
|
||||
|
||||
if [[ ! -f "$KERNEL" ]] || [[ ! -f "$ROOTFS" ]]; then
|
||||
echo "ERROR: Pi 3 full-system images not found in $IMG_DIR" >&2
|
||||
echo "" >&2
|
||||
echo "To set up full-system emulation:" >&2
|
||||
echo " 1. Download Raspberry Pi OS Lite (64-bit) from raspberrypi.com" >&2
|
||||
echo " 2. Extract kernel8.img from the boot partition" >&2
|
||||
echo " 3. Get the DTB: bcm2710-rpi-3-b.dtb from the boot partition" >&2
|
||||
echo " 4. Place all in: $IMG_DIR/" >&2
|
||||
echo "" >&2
|
||||
echo "User-mode testing (default) works without these files." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create overlay so we don't modify the base image
|
||||
qemu-img create -f qcow2 -b "$ROOTFS" "$OVERLAY" 2>/dev/null || true
|
||||
|
||||
exec "$QEMU_SYS" \
|
||||
-M raspi3b \
|
||||
-kernel "$KERNEL" \
|
||||
-dtb "$DTB" \
|
||||
-drive "file=$OVERLAY,if=sd,format=qcow2" \
|
||||
-m 1G \
|
||||
-smp 4 \
|
||||
-nographic \
|
||||
-append "console=ttyAMA0 root=/dev/mmcblk0p2 rootwait rw quiet"
|
||||
}
|
||||
|
||||
case "$PI_MODEL" in
|
||||
pi3|pi4|pi5)
|
||||
# All three use the same aarch64 binary — the binary is
|
||||
# architecture-compatible across Cortex-A53/A72/A76.
|
||||
# The model name documents which hardware variant is being simulated.
|
||||
run_user_mode "$TRI_AARCH64_BIN" "$PI_MODEL (Cortex-A*)"
|
||||
"$@"
|
||||
;;
|
||||
pi3-cli|pi4-cli|pi5-cli)
|
||||
run_user_mode "$TRI_AARCH64_CLI" "$PI_MODEL CLI" "$@"
|
||||
;;
|
||||
pi3-full)
|
||||
run_full_system_pi3
|
||||
;;
|
||||
*)
|
||||
echo "Unknown model: $PI_MODEL" >&2
|
||||
echo "Supported: pi3, pi4, pi5, pi3-cli, pi4-cli, pi5-cli, pi3-full" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,131 @@
|
||||
// Fuzz harness for CTransaction deserialization.
|
||||
//
|
||||
// Compile via the BUILD_FUZZ=ON path (see src/CMakeLists.txt):
|
||||
// cmake -G Ninja -DBUILD_TESTS=ON -DBUILD_FUZZ=ON ..
|
||||
// ninja transaction_deserialize_fuzz
|
||||
//
|
||||
// Run:
|
||||
// ./bin/transaction_deserialize_fuzz -max_total_time=300 -max_len=200000 corpus/
|
||||
// ./bin/transaction_deserialize_fuzz crash-deadbeef.bin
|
||||
//
|
||||
// Input format (libFuzzer): raw bytes that get fed straight into the
|
||||
// Bitcoin-style deserializer. The fuzz target is deliberately raw
|
||||
// bytes (no framing): it exercises ReadCompactSize + nested
|
||||
// Unserialize_impl<uint8_t> / Unserialize_impl<CTxIn> /
|
||||
// Unserialize_impl<CTxOut> with arbitrary attacker-controlled input.
|
||||
//
|
||||
// What this covers:
|
||||
// * CompactSize varint decoder (ReadCompactSize) — every overflow /
|
||||
// truncation / non-canonical encoding path.
|
||||
// * Vector<T> Unserialize_impl — recursive expansion when T is itself
|
||||
// a structured type (CTxIn / CTxOut). Known to do unbounded
|
||||
// std::vector::resize(nSize) before reading; this is the historical
|
||||
// DoS surface for "send a tx claiming nSize=0xFFFFFFFF".
|
||||
// * CScript deserialization (a vector<unsigned char> with script
|
||||
// bytes that downstream EvalScript consumes — the script_fuzz target
|
||||
// covers the EvalScript side; this covers the deserialize-side).
|
||||
// * CTransaction::CheckTransaction bounds (max size, negative value,
|
||||
// out-of-range totals) — these run AFTER the deserialize and reject
|
||||
// the parsed object. Fuzzing the deserialize+Check pair surfaces
|
||||
// any path where the parse side consumes unbounded resources before
|
||||
// the Check rejects.
|
||||
// * Hash determinism — GetHash() must produce the same uint256 for
|
||||
// the same bytes, regardless of intermediate state mutations.
|
||||
//
|
||||
// What this does NOT cover:
|
||||
// * Signature verification (needs CKey + a CTransaction; that's
|
||||
// covered by the existing script_tests.cpp and keystore_tests.cpp).
|
||||
// * Block-level validation (block_deserialize_fuzz would be the next
|
||||
// target if this proves its value).
|
||||
// * P2P message framing (the fuzz input is the raw tx payload, not
|
||||
// the wire envelope — the wire envelope goes through CNode / net
|
||||
// code, not the tx parser).
|
||||
//
|
||||
// Why this is the right second target:
|
||||
// Every peer message body starts with a deserialize step. Bugs in this
|
||||
// surface are attacker-reachable from any peer who can pass IP filters,
|
||||
// so the blast radius is the entire p2p network. Bitcoin Core maintains
|
||||
// `deserialize-fuzz` for tx, block, and p2p-message surfaces for the
|
||||
// same reason — the cost of writing it is low (about 40 lines) and the
|
||||
// historical bug rate is non-zero.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
#include "main.h"
|
||||
#include "serialize.h"
|
||||
#include "uint256.h"
|
||||
|
||||
// Read a single transaction from the input buffer.
|
||||
//
|
||||
// We construct a CDataStream from the fuzz input and call
|
||||
// Unserialize directly. That exercises the SAME code path the daemon
|
||||
// uses when receiving a "tx" P2P message — the wire payload is exactly
|
||||
// the byte sequence that lands in Unserialize().
|
||||
//
|
||||
// The CDataStream machinery handles stream-state (eof, throw-on-truncation)
|
||||
// the same way for both network reads and our in-memory buffer.
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
|
||||
{
|
||||
if (size == 0) return 0;
|
||||
|
||||
CDataStream ds(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ds.write(reinterpret_cast<const char*>(data), size);
|
||||
|
||||
try
|
||||
{
|
||||
CTransaction tx;
|
||||
ds >> tx;
|
||||
|
||||
// tx is now in some consistent or inconsistent state. We don't
|
||||
// care about validity — only that no input crashes, leaks, or
|
||||
// trips UBSan. Two post-parse sanity probes:
|
||||
|
||||
// 1) Hash must be deterministic for any well-formed CTransaction
|
||||
// object. A divergent hash indicates corrupted state in
|
||||
// SerializeHash (we hash and discard the result, just to
|
||||
// ensure the call doesn't UB).
|
||||
(void)tx.GetHash();
|
||||
|
||||
// 2) Round-trip serialize must produce a stream that re-parses
|
||||
// to the same GetHash(). This catches bugs where a struct
|
||||
// field is dropped or scrambled during deserialization.
|
||||
CDataStream ds2(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ds2 << tx;
|
||||
CTransaction tx2;
|
||||
ds2 >> tx2;
|
||||
if (tx2.GetHash() != tx.GetHash())
|
||||
{
|
||||
// Non-fatal — flag for inspection by writing to stderr so
|
||||
// the fuzzer log surfaces it. The fuzzer won't be killed.
|
||||
std::fprintf(stderr,
|
||||
"WARN: round-trip hash mismatch — deserialization loses information\n");
|
||||
}
|
||||
|
||||
// 3) CheckTransaction bounds — should NOT crash even on garbage
|
||||
// data, just return false. This is the post-parse validator
|
||||
// that catches oversized / negative / out-of-range txs.
|
||||
(void)tx.CheckTransaction();
|
||||
}
|
||||
catch (const std::exception&)
|
||||
{
|
||||
// std::ios_base::failure from CDataStream on truncation, or
|
||||
// std::runtime_error from any Unserialize_impl check. These
|
||||
// are EXPECTED for malicious input — the daemon catches and
|
||||
// drops the peer, no UB or crash should result.
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Unknown exception — log so the fuzzer surfaces it. LibFuzzer
|
||||
// doesn't catch C++ exceptions thrown out of LLVMFuzzerTestOneInput;
|
||||
// they would terminate the process. Returning 0 keeps the
|
||||
// process alive so the fuzzer continues probing.
|
||||
std::fprintf(stderr, "WARN: unknown exception in tx deserialize\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cross-compile libtor.a for aarch64
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TOR_SRC_DIR="${TOR_SRC_DIR:-$ROOT_DIR/tor-src}"
|
||||
|
||||
if [[ ! -d "$TOR_SRC_DIR" ]]; then
|
||||
echo "Tor source tree not found at: $TOR_SRC_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$TOR_SRC_DIR"
|
||||
|
||||
# Use vendored configure (same as the native build)
|
||||
VENDORED_CONFIGURE="$ROOT_DIR/configure.vendored"
|
||||
VENDORED_AUX_DIR="$ROOT_DIR/configure-aux"
|
||||
VENDORED_INPUT_DIR="$ROOT_DIR/configure-input"
|
||||
|
||||
if [[ -f "$VENDORED_CONFIGURE" ]]; then
|
||||
echo "Using vendored configure from $VENDORED_CONFIGURE"
|
||||
cp -f "$VENDORED_CONFIGURE" "./configure"
|
||||
chmod +x ./configure
|
||||
if [[ -d "$VENDORED_AUX_DIR" ]]; then
|
||||
cp -f "$VENDORED_AUX_DIR"/* ./
|
||||
chmod +x ./ar-lib ./compile ./config.guess ./config.sub \
|
||||
./depcomp ./install-sh ./missing ./test-driver 2>/dev/null || true
|
||||
fi
|
||||
if [[ -d "$VENDORED_INPUT_DIR" ]]; then
|
||||
cp -rf "$VENDORED_INPUT_DIR"/. ./
|
||||
find . -name '*.in' -o -name 'aclocal.m4' | xargs touch 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Configuring Tor for aarch64 cross-compile ==="
|
||||
CC=aarch64-linux-gnu-gcc \
|
||||
CXX=aarch64-linux-gnu-g++ \
|
||||
AR=aarch64-linux-gnu-ar \
|
||||
RANLIB=aarch64-linux-gnu-ranlib \
|
||||
STRIP=aarch64-linux-gnu-strip \
|
||||
./configure \
|
||||
--host=aarch64-linux-gnu \
|
||||
--disable-asciidoc \
|
||||
--disable-manpage \
|
||||
--disable-html-manual \
|
||||
--disable-system-torrc \
|
||||
--disable-systemd \
|
||||
--disable-lzma \
|
||||
--disable-zstd \
|
||||
--disable-nss \
|
||||
--enable-pic \
|
||||
--enable-static-libevent \
|
||||
--with-openssl-dir=/usr \
|
||||
--with-libevent-dir=/usr \
|
||||
--with-zlib-dir=/usr \
|
||||
LIBS="-L/usr/lib/aarch64-linux-gnu" \
|
||||
CPPFLAGS="-I/usr/include" \
|
||||
LDFLAGS="-L/usr/lib/aarch64-linux-gnu"
|
||||
|
||||
echo "=== Building libtor.a for aarch64 ==="
|
||||
make -j$(nproc) libor.a libtor.a 2>&1 || make -j$(nproc) 2>&1
|
||||
|
||||
echo "=== Result ==="
|
||||
ls -lh "$TOR_SRC_DIR/libtor.a" 2>/dev/null && echo "SUCCESS: libtor.a built for aarch64" || echo "FAILED"
|
||||
file "$TOR_SRC_DIR/libtor.a" 2>/dev/null
|
||||
Reference in New Issue
Block a user