From 117365d99e077be87303f172720167a1ef5e2ae8 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 18 Oct 2026 17:01:54 +0700 Subject: [PATCH 1/2] build(native): read bridge toolchains from their pin files --- .claude/rules/native-bridges.md | 2 +- CONTRIBUTING.md | 4 +- Native/DamengBridge/README.md | 1 + Native/HanaBridge/THIRD_PARTY_NOTICES.md | 4 +- scripts/build-dameng.sh | 57 +++++++++--------------- scripts/build-hana.sh | 50 +++++---------------- scripts/lib/deployment-target.sh | 43 +++++++++++++++--- scripts/lib/macos.sh | 37 --------------- 8 files changed, 75 insertions(+), 123 deletions(-) diff --git a/.claude/rules/native-bridges.md b/.claude/rules/native-bridges.md index a77c344421..df50c7a0a4 100644 --- a/.claude/rules/native-bridges.md +++ b/.claude/rules/native-bridges.md @@ -11,5 +11,5 @@ paths: - **First-party bridge code lives in `Native/Bridge`**, built by `scripts/build-.sh`, with output in a gitignored folder beside the source, never in `Libs/`. - **Rust may run in the app process; Go may not.** The Dameng staticlib is built with `panic = "unwind"`, and every export that does driver work wraps it in `catch_unwind`; a new export that does work takes the same guard. Go's `recover()` cannot catch a panic on a goroutine go-hdb starts, so HANA runs out of process as `tablepro-hana-helper`, one per driver session. -- **Toolchains and upstream code are pinned**: `rust-toolchain.toml`, `Cargo.lock` with `--locked`, the `toolchain` line of `go.mod`, `go.sum`. A protocol change for Dameng lands on the [TableProApp/rust-dameng](https://github.com/TableProApp/rust-dameng) fork first, then the pinned `rev` moves. +- **Toolchains and upstream code are pinned**: `rust-toolchain.toml`, `Cargo.lock` with `--locked`, the `toolchain` line of `go.mod`, `go.sum`. The build scripts read the toolchains from these files and write no version of their own. A protocol change for Dameng lands on the [TableProApp/rust-dameng](https://github.com/TableProApp/rust-dameng) fork first, then the pinned `rev` moves. - **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary. The `HanaHelperEmbedding` copy phase in `project.yml` signs the HANA helper with the plugin's identity, and `scripts/release/build.sh plugins` checks every executable with `codesign -dvvv`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03d1253aca..e66ed80351 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,8 @@ | XcodeGen | 2.46.0 exactly | The generated project differs between versions, so `scripts/generate-project.sh` warns on any other | | SwiftLint | 0.65.1 | The version CI runs | | GitHub CLI | any | Optional: `scripts/download-libs.sh` falls back to `curl` | -| Go | 1.27.1 | SAP HANA plugin only: run `scripts/build-hana.sh` with no argument before building `HanaDriver` or `AllPlugins`. The script fetches 1.27.1 when the installed `go` is another release | -| Rust | 1.91.1 | Dameng plugin only, through [rustup](https://rustup.rs): run `scripts/build-dameng.sh arm64` before building `DamengDriver` or `AllPlugins` | +| Go | `toolchain` in `Native/HanaBridge/go.mod` | SAP HANA plugin only: run `scripts/build-hana.sh` with no argument before building `HanaDriver` or `AllPlugins`. When the installed `go` is another release, `go` switches to that toolchain itself | +| Rust | `Native/DamengBridge/rust-toolchain.toml` | Dameng plugin only, through [rustup](https://rustup.rs), which installs it: run `scripts/build-dameng.sh arm64` before building `DamengDriver` or `AllPlugins` | Fork the repo on GitHub, then: diff --git a/Native/DamengBridge/README.md b/Native/DamengBridge/README.md index a745755c09..612d8b22aa 100644 --- a/Native/DamengBridge/README.md +++ b/Native/DamengBridge/README.md @@ -32,5 +32,6 @@ To change protocol behaviour, commit to the fork's `tablepro` branch, then bump scripts/build-dameng.sh [arm64|x86_64|both] ``` +`rust-toolchain.toml` pins the Rust release and both targets; the script has rustup install them. Fetching a git dependency needs network access. Xcode does not build this; the plugin target links the archive `build-dameng.sh` produces, so build it before building `DamengDriver`. diff --git a/Native/HanaBridge/THIRD_PARTY_NOTICES.md b/Native/HanaBridge/THIRD_PARTY_NOTICES.md index 6fa7a2f436..15e6bf7ccc 100644 --- a/Native/HanaBridge/THIRD_PARTY_NOTICES.md +++ b/Native/HanaBridge/THIRD_PARTY_NOTICES.md @@ -8,7 +8,7 @@ everything below is statically linked into it. The helper ships inside the SAP H | --- | --- | --- | --- | | [go-hdb](https://github.com/SAP/go-hdb) | v1.18.12 | Apache-2.0 | 2014-2026 SAP SE or an SAP affiliate company and go-hdb contributors | | [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) | v0.42.0 | BSD-3-Clause | 2009 The Go Authors | -| [Go runtime and standard library](https://go.dev) | go1.27.1 | BSD-3-Clause | 2009 The Go Authors | +| [Go runtime and standard library](https://go.dev) | go1.27.2 | BSD-3-Clause | 2009 The Go Authors | The Go standard library vendors golang.org/x/crypto, x/net, x/sys and x/text. They carry the same BSD-3-Clause notice from The Go Authors as the standard library itself. @@ -20,6 +20,6 @@ The full license texts: - go-hdb: - golang.org/x/text: -- Go: +- Go: The app's acknowledgements carry the same three texts in `TablePro/Resources/ThirdPartyLicenses/texts/`. diff --git a/scripts/build-dameng.sh b/scripts/build-dameng.sh index a84490a7c1..c809f50a2f 100755 --- a/scripts/build-dameng.sh +++ b/scripts/build-dameng.sh @@ -7,54 +7,40 @@ BRIDGE_DIR="$ROOT_DIR/Native/DamengBridge" # download-libs.sh, and publish-libs.sh treats anything unrecognised there as an error. # The bridge is compiled from in-tree Rust instead, so its output stays beside its source. LIBS_DIR="${LIBS_DIR:-$BRIDGE_DIR/lib}" +OUTPUT="$LIBS_DIR/libdameng_bridge.a" ARCH="${1:-both}" source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/deployment-target.sh" -MACOS_TARGET="$DEPLOY_TARGET" -RUST_TOOLCHAIN="1.91.1" -if ! rustup toolchain list | grep -q "^${RUST_TOOLCHAIN}"; then - rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal -fi +# rustup takes the toolchain, profile and targets from rust-toolchain.toml in the working directory. +(cd "$BRIDGE_DIR" && rustup toolchain install) -build_slice() { - local arch="$1" - local target="$2" - local output="$LIBS_DIR/libdameng_bridge_${arch}.a" - - if ! rustup target list --installed --toolchain "$RUST_TOOLCHAIN" | grep -qx "$target"; then - rustup target add "$target" --toolchain "$RUST_TOOLCHAIN" - fi - - env RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" MACOSX_DEPLOYMENT_TARGET="$MACOS_TARGET" \ - cargo build \ - --manifest-path "$BRIDGE_DIR/Cargo.toml" \ - --locked \ - --release \ - --target "$target" +archive() { + echo "$BRIDGE_DIR/target/$1/release/libtablepro_dameng_bridge.a" +} - cp "$BRIDGE_DIR/target/$target/release/libtablepro_dameng_bridge.a" "$output" - nm -gU "$output" | grep 'T _tp_dm_connect' > /dev/null +build_slice() { + ( + cd "$BRIDGE_DIR" + env MACOSX_DEPLOYMENT_TARGET="$DEPLOY_TARGET" cargo build --locked --release --target "$1" + ) + nm -gU "$(archive "$1")" | grep 'T _tp_dm_connect' > /dev/null } mkdir -p "$LIBS_DIR" case "$ARCH" in arm64) - build_slice arm64 aarch64-apple-darwin - cp "$LIBS_DIR/libdameng_bridge_arm64.a" "$LIBS_DIR/libdameng_bridge.a" + build_slice aarch64-apple-darwin + cp "$(archive aarch64-apple-darwin)" "$OUTPUT" ;; x86_64) - build_slice x86_64 x86_64-apple-darwin - cp "$LIBS_DIR/libdameng_bridge_x86_64.a" "$LIBS_DIR/libdameng_bridge.a" + build_slice x86_64-apple-darwin + cp "$(archive x86_64-apple-darwin)" "$OUTPUT" ;; - both|universal) - build_slice arm64 aarch64-apple-darwin - build_slice x86_64 x86_64-apple-darwin - lipo -create \ - "$LIBS_DIR/libdameng_bridge_arm64.a" \ - "$LIBS_DIR/libdameng_bridge_x86_64.a" \ - -output "$LIBS_DIR/libdameng_bridge_universal.a" - cp "$LIBS_DIR/libdameng_bridge_universal.a" "$LIBS_DIR/libdameng_bridge.a" + both) + build_slice aarch64-apple-darwin + build_slice x86_64-apple-darwin + lipo -create "$(archive aarch64-apple-darwin)" "$(archive x86_64-apple-darwin)" -output "$OUTPUT" ;; *) echo "Usage: $0 [arm64|x86_64|both]" >&2 @@ -62,4 +48,5 @@ case "$ARCH" in ;; esac -file "$LIBS_DIR"/libdameng_bridge*.a +lipo -info "$OUTPUT" +verify_deployment_target "$OUTPUT" diff --git a/scripts/build-hana.sh b/scripts/build-hana.sh index acf266dbcc..c39fe24f09 100755 --- a/scripts/build-hana.sh +++ b/scripts/build-hana.sh @@ -6,7 +6,6 @@ BRIDGE_DIR="$ROOT_DIR/Native/HanaBridge" BIN_DIR="$BRIDGE_DIR/bin" HELPER_NAME="tablepro-hana-helper" OUTPUT="$BIN_DIR/$HELPER_NAME" -GO_TOOLCHAIN="go1.27.1" ARCH="${1:-both}" source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/deployment-target.sh" @@ -15,6 +14,13 @@ if ! command -v go > /dev/null; then exit 1 fi +# Named exactly: the default GOTOOLCHAIN=auto treats this line as a minimum and keeps a newer local go. +GO_TOOLCHAIN="$(awk '$1 == "toolchain" { print $2 }' "$BRIDGE_DIR/go.mod")" +if [ -z "$GO_TOOLCHAIN" ]; then + echo "$BRIDGE_DIR/go.mod has no toolchain line" >&2 + exit 1 +fi + go_arch() { case "$1" in arm64) echo arm64 ;; @@ -32,40 +38,6 @@ build_slice() { ) } -version_at_most() { - awk -v actual="$1" -v limit="$2" 'BEGIN { - split(actual, a, ".") - split(limit, l, ".") - for (i = 1; i <= 3; i++) { - if ((a[i] + 0) < (l[i] + 0)) exit 0 - if ((a[i] + 0) > (l[i] + 0)) exit 1 - } - exit 0 - }' -} - -slice_minimum_os() { - vtool -arch "$2" -show-build "$1" | awk '$1 == "minos" { print $2; exit }' -} - -verify_minimum_os() { - local binary="$1" - local arch - local version - for arch in $(lipo -archs "$binary"); do - version="$(slice_minimum_os "$binary" "$arch")" - if [ -z "$version" ]; then - echo "$binary ($arch) records no minimum macOS" >&2 - exit 1 - fi - if ! version_at_most "$version" "$DEPLOY_TARGET"; then - echo "$binary ($arch) targets macOS $version, above the deployment target $DEPLOY_TARGET" >&2 - exit 1 - fi - echo "$arch: minimum macOS $version, deployment target $DEPLOY_TARGET" - done -} - mkdir -p "$BIN_DIR" case "$ARCH" in @@ -73,7 +45,7 @@ case "$ARCH" in build_slice "$ARCH" cp "$BIN_DIR/$HELPER_NAME-$ARCH" "$OUTPUT" ;; - both|universal) + both) build_slice arm64 build_slice x86_64 lipo -create "$BIN_DIR/$HELPER_NAME-arm64" "$BIN_DIR/$HELPER_NAME-x86_64" -output "$OUTPUT" @@ -84,6 +56,6 @@ case "$ARCH" in ;; esac -codesign --force -s - "$OUTPUT" -file "$OUTPUT" -verify_minimum_os "$OUTPUT" +codesign --force -o runtime -s - "$OUTPUT" +lipo -info "$OUTPUT" +verify_deployment_target "$OUTPUT" diff --git a/scripts/lib/deployment-target.sh b/scripts/lib/deployment-target.sh index 2b879bc6fd..314088b8f0 100644 --- a/scripts/lib/deployment-target.sh +++ b/scripts/lib/deployment-target.sh @@ -1,10 +1,6 @@ -# The macOS deployment target every native library is built for, read from the one place that -# decides it. -# -# Sourced, never executed, and it defines nothing but DEPLOY_TARGET so a script with its own -# LIBS_DIR or REPO_ROOT can pick it up without inheriting anything else: -# -# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/deployment-target.sh" +# The macOS deployment target every native library and bridge is built for, read from project.yml, +# and the check that a built Mach-O stays at or below it. Sourced, never executed. Unlike common.sh +# it sets no LIBS_DIR or REPO_ROOT, so a script with its own can source it. # # shellcheck shell=bash # shellcheck disable=SC2034 # DEPLOY_TARGET is read by the scripts that source this @@ -26,3 +22,36 @@ if [ -z "$DEPLOY_TARGET" ]; then echo "Could not read deploymentTarget.macOS from $TABLEPRO_DEPLOY_TARGET_ROOT/project.yml" >&2 exit 1 fi + +# The highest minimum macOS across every slice and archive member, from LC_BUILD_VERSION or the +# older LC_VERSION_MIN_MACOSX: one newer object is enough to keep the whole file off the floor. +macho_min_version() { + otool -arch all -l "$1" 2> /dev/null | awk ' + $1 == "cmd" { cmd = $2; next } + cmd == "LC_BUILD_VERSION" && $1 == "minos" { print $2 } + cmd == "LC_VERSION_MIN_MACOSX" && $1 == "version" { print $2 } + ' | sort -V | tail -1 +} + +verify_deployment_target() { + local file name version failed=0 + echo "" + echo "🔍 Verifying deployment targets..." + for file in "$@"; do + [ -f "$file" ] || continue + name=$(basename "$file") + if ! version=$(macho_min_version "$file") || [ -z "$version" ]; then + echo " ❌ $name records no minimum macOS" + failed=1 + elif [ "$(printf '%s\n' "$DEPLOY_TARGET" "$version" | sort -V | tail -1)" != "$DEPLOY_TARGET" ]; then + echo " ❌ $name targets macOS $version (expected $DEPLOY_TARGET)" + failed=1 + else + echo " ✅ $name targets macOS $version" + fi + done + if [ "$failed" -eq 1 ]; then + echo "❌ FATAL: some libraries have incorrect deployment targets" >&2 + return 1 + fi +} diff --git a/scripts/lib/macos.sh b/scripts/lib/macos.sh index 4fec055602..0b9bfa5e16 100644 --- a/scripts/lib/macos.sh +++ b/scripts/lib/macos.sh @@ -32,43 +32,6 @@ make_universal() { done } -# Reads the macOS version a Mach-O was built for. Prefers LC_BUILD_VERSION and falls back to the -# older LC_VERSION_MIN_MACOSX. `tail -1` takes the highest across the archive's members, which is -# the one that decides whether the whole library can run on the app's floor. -macho_min_version() { - local lib="$1" version - version=$(otool -l "$lib" 2>/dev/null | - awk '/LC_BUILD_VERSION/{found=1} found && /minos/{print $2; found=0}' | sort -V | tail -1) - if [ -z "$version" ]; then - version=$(otool -l "$lib" 2>/dev/null | - awk '/LC_VERSION_MIN_MACOSX/{found=1} found && /version/{print $2; found=0}' | sort -V | tail -1) - fi - printf '%s' "$version" -} - -# Fails on a library built for a newer macOS than DEPLOY_TARGET; an older one runs fine. -verify_deployment_target() { - local lib name version failed=0 - echo "" - echo "🔍 Verifying deployment targets..." - for lib in "$@"; do - [ -f "$lib" ] || continue - name=$(basename "$lib") - version=$(macho_min_version "$lib") - [ -n "$version" ] || continue - if [ "$(printf '%s\n' "$DEPLOY_TARGET" "$version" | sort -V | tail -1)" != "$DEPLOY_TARGET" ]; then - echo " ❌ $name targets macOS $version (expected $DEPLOY_TARGET)" - failed=1 - else - echo " ✅ $name targets macOS $version" - fi - done - if [ "$failed" -eq 1 ]; then - echo "❌ FATAL: some libraries have incorrect deployment targets" >&2 - return 1 - fi -} - fetch_openssl() { local tarball="$BUILD_DIR/openssl-$OPENSSL_VERSION.tar.gz" if [ ! -f "$tarball" ]; then From 79403e6f025cb9c36081301fe8e438436c9f425e Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 18 Oct 2026 18:48:13 +0700 Subject: [PATCH 2/2] build(native): read bridge toolchains from their pin files --- .claude/rules/native-bridges.md | 2 +- changelog.d/3513.security.md | 1 + scripts/build-dameng.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/3513.security.md diff --git a/.claude/rules/native-bridges.md b/.claude/rules/native-bridges.md index df50c7a0a4..a77c344421 100644 --- a/.claude/rules/native-bridges.md +++ b/.claude/rules/native-bridges.md @@ -11,5 +11,5 @@ paths: - **First-party bridge code lives in `Native/Bridge`**, built by `scripts/build-.sh`, with output in a gitignored folder beside the source, never in `Libs/`. - **Rust may run in the app process; Go may not.** The Dameng staticlib is built with `panic = "unwind"`, and every export that does driver work wraps it in `catch_unwind`; a new export that does work takes the same guard. Go's `recover()` cannot catch a panic on a goroutine go-hdb starts, so HANA runs out of process as `tablepro-hana-helper`, one per driver session. -- **Toolchains and upstream code are pinned**: `rust-toolchain.toml`, `Cargo.lock` with `--locked`, the `toolchain` line of `go.mod`, `go.sum`. The build scripts read the toolchains from these files and write no version of their own. A protocol change for Dameng lands on the [TableProApp/rust-dameng](https://github.com/TableProApp/rust-dameng) fork first, then the pinned `rev` moves. +- **Toolchains and upstream code are pinned**: `rust-toolchain.toml`, `Cargo.lock` with `--locked`, the `toolchain` line of `go.mod`, `go.sum`. A protocol change for Dameng lands on the [TableProApp/rust-dameng](https://github.com/TableProApp/rust-dameng) fork first, then the pinned `rev` moves. - **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary. The `HanaHelperEmbedding` copy phase in `project.yml` signs the HANA helper with the plugin's identity, and `scripts/release/build.sh plugins` checks every executable with `codesign -dvvv`. diff --git a/changelog.d/3513.security.md b/changelog.d/3513.security.md new file mode 100644 index 0000000000..ed11f95d51 --- /dev/null +++ b/changelog.d/3513.security.md @@ -0,0 +1 @@ +SAP HANA plugin 1.0.1 to 1.0.3 shipping a helper built with Go 1.27.1, without the crypto/tls fix for GO-2026-6607. diff --git a/scripts/build-dameng.sh b/scripts/build-dameng.sh index c809f50a2f..7aa2bcb424 100755 --- a/scripts/build-dameng.sh +++ b/scripts/build-dameng.sh @@ -12,7 +12,7 @@ ARCH="${1:-both}" source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/deployment-target.sh" # rustup takes the toolchain, profile and targets from rust-toolchain.toml in the working directory. -(cd "$BRIDGE_DIR" && rustup toolchain install) +(cd "$BRIDGE_DIR" && rustup toolchain install --no-self-update) archive() { echo "$BRIDGE_DIR/target/$1/release/libtablepro_dameng_bridge.a"