From 3dfd11da1174980897d21c4352dfa4cdb91a2e59 Mon Sep 17 00:00:00 2001 From: Xiao Deshi Date: Sun, 18 Oct 2026 19:37:23 +0800 Subject: [PATCH 1/3] docs(tandem): document Linux-only build (macOS unsupported) --- tandem/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tandem/README.md diff --git a/tandem/README.md b/tandem/README.md new file mode 100644 index 000000000..eee4ce534 --- /dev/null +++ b/tandem/README.md @@ -0,0 +1,12 @@ + +## Platform support + +Tandem builds only on Linux. Its native C dependencies (nghttp2, c-ares, +mbedtls, cpucycles) are cross-compiled with `tools/build_native.sh`, which +invokes CMake + Ninja with a `zig cc -target ` toolchain; the CMake +probe injects macOS-host-only flags (`-arch`, `-isysroot`) that zig cc rejects +for non-host targets. CI therefore builds and tests Tandem exclusively on +Linux runners (`zig build persistence-probe test -Dtarget=x86_64-linux-musl`). + +On macOS hosts use `make` (which builds `sandboxd` and the Go binaries only) +and rely on CI for Tandem verification. From f9ef640aa71be27439db0c2edde0f0241a40a2ab Mon Sep 17 00:00:00 2001 From: Xiao Deshi Date: Sun, 18 Oct 2026 20:38:15 +0800 Subject: [PATCH 2/3] chore(tandem): anchor grpc-lite to v0.5.0 and review tandem code --- .../vendor/grpc-lite/.github/workflows/ci.yml | 54 ++++-- .../grpc-lite/.github/workflows/coverage.yml | 4 +- .../workflows/release-transpiled-c.yml | 59 ++++-- .../.github/workflows/repeated-tests.yml | 2 +- .../grpc-lite/.github/workflows/soak.yml | 4 +- tandem/vendor/grpc-lite/AGENTS.md | 7 +- tandem/vendor/grpc-lite/CMakeLists.txt | 20 +- tandem/vendor/grpc-lite/README.md | 9 +- tandem/vendor/grpc-lite/build.zig | 3 - tandem/vendor/grpc-lite/build.zig.zon | 12 +- .../cmake/grpc_liteConfigVersion.cmake | 2 +- tandem/vendor/grpc-lite/docs/c-cpp-api.md | 32 +++- tandem/vendor/grpc-lite/docs/development.md | 16 +- tandem/vendor/grpc-lite/docs/zig-api.md | 34 ++++ tandem/vendor/grpc-lite/src/c_api.zig | 37 +++- tandem/vendor/grpc-lite/src/channel.zig | 84 ++++++++- .../vendor/grpc-lite/src/cpucycles_compat.c | 8 +- tandem/vendor/grpc-lite/src/root.zig | 1 + tandem/vendor/grpc-lite/src/server.zig | 36 +++- .../vendor/grpc-lite/src/testdata/mtls/ca.pem | 11 ++ .../src/testdata/mtls/client-key.pem | 5 + .../grpc-lite/src/testdata/mtls/client.pem | 11 ++ .../src/testdata/mtls/expired-client.pem | 11 ++ .../grpc-lite/src/testdata/mtls/generate.sh | 34 ++++ tandem/vendor/grpc-lite/src/tls_record.zig | 166 +++++++++++++++-- .../grpc-lite/tests/consumer/run_cmake_c.sh | 16 ++ .../vendor/grpc-lite/tests/official/README.md | 8 + tandem/vendor/grpc-lite/tests/official/go.mod | 4 + .../tests/official/interop_client.zig | 34 +++- .../tests/official/interop_server.zig | 11 ++ .../tests/official/mtls_peer/main.go | 72 +++++++ .../grpc-lite/tests/official/run_tls.sh | 40 ++++ tandem/vendor/grpc-lite/tools/build_native.sh | 175 +++++++----------- .../grpc-lite/tools/mbedtls_user_config.h | 4 + .../grpc-lite/tools/package_transpiled_c.sh | 7 +- tandem/vendor/grpc-lite/tools/transpile_c.sh | 10 +- .../grpc-lite/tools/transpiled_c_target.sh | 16 ++ 37 files changed, 845 insertions(+), 214 deletions(-) create mode 100644 tandem/vendor/grpc-lite/src/testdata/mtls/ca.pem create mode 100644 tandem/vendor/grpc-lite/src/testdata/mtls/client-key.pem create mode 100644 tandem/vendor/grpc-lite/src/testdata/mtls/client.pem create mode 100644 tandem/vendor/grpc-lite/src/testdata/mtls/expired-client.pem create mode 100644 tandem/vendor/grpc-lite/src/testdata/mtls/generate.sh create mode 100644 tandem/vendor/grpc-lite/tests/official/mtls_peer/main.go create mode 100644 tandem/vendor/grpc-lite/tools/transpiled_c_target.sh diff --git a/tandem/vendor/grpc-lite/.github/workflows/ci.yml b/tandem/vendor/grpc-lite/.github/workflows/ci.yml index d8df62e8c..abd5e891a 100644 --- a/tandem/vendor/grpc-lite/.github/workflows/ci.yml +++ b/tandem/vendor/grpc-lite/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -71,7 +71,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -93,7 +93,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -110,8 +110,20 @@ jobs: generated-c-bundle: name: Generated C bundle - runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + target: x86_64-linux-gnu + - arch: arm64 + runner: ubuntu-24.04-arm + target: aarch64-linux-gnu + runs-on: ${{ matrix.runner }} timeout-minutes: 30 + env: + GRPC_LITE_TRANSPILED_C_TARGET: ${{ matrix.target }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -120,7 +132,7 @@ jobs: persist-credentials: false - name: Install pinned mise and Zig - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -128,9 +140,9 @@ jobs: run: mise run check-transpiled-c - name: Upload verified bundle - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: grpc-lite-transpiled-c + name: grpc-lite-transpiled-c-${{ matrix.arch }} path: .zig-cache/transpiled-c-dist/* if-no-files-found: error include-hidden-files: true @@ -139,8 +151,20 @@ jobs: generated-c-consumer: name: Generated C bundle consumer — no Zig/no network needs: generated-c-bundle - runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + target: x86_64-linux-gnu + - arch: arm64 + runner: ubuntu-24.04-arm + target: aarch64-linux-gnu + runs-on: ${{ matrix.runner }} timeout-minutes: 45 + env: + GRPC_LITE_TRANSPILED_C_TARGET: ${{ matrix.target }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -154,9 +178,9 @@ jobs: docker version - name: Download verified bundle - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - name: grpc-lite-transpiled-c + name: grpc-lite-transpiled-c-${{ matrix.arch }} path: bundle-dist - name: Consume bundle without Zig or network @@ -179,7 +203,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -207,7 +231,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -234,7 +258,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -263,7 +287,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -292,7 +316,7 @@ jobs: run: mise run interop-http2-edge - name: Upload interop reports - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: name: interop-reports-${{ matrix.arch }} diff --git a/tandem/vendor/grpc-lite/.github/workflows/coverage.yml b/tandem/vendor/grpc-lite/.github/workflows/coverage.yml index 357b453bd..d43e98ac8 100644 --- a/tandem/vendor/grpc-lite/.github/workflows/coverage.yml +++ b/tandem/vendor/grpc-lite/.github/workflows/coverage.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -41,7 +41,7 @@ jobs: run: mise run coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: disable_search: true fail_ci_if_error: true diff --git a/tandem/vendor/grpc-lite/.github/workflows/release-transpiled-c.yml b/tandem/vendor/grpc-lite/.github/workflows/release-transpiled-c.yml index 806cef934..647bc4c23 100644 --- a/tandem/vendor/grpc-lite/.github/workflows/release-transpiled-c.yml +++ b/tandem/vendor/grpc-lite/.github/workflows/release-transpiled-c.yml @@ -26,8 +26,20 @@ jobs: needs: full-validation permissions: contents: read - runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + target: x86_64-linux-gnu + - arch: arm64 + runner: ubuntu-24.04-arm + target: aarch64-linux-gnu + runs-on: ${{ matrix.runner }} timeout-minutes: 30 + env: + GRPC_LITE_TRANSPILED_C_TARGET: ${{ matrix.target }} steps: - name: Checkout event commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -47,7 +59,7 @@ jobs: test "$version" = "$config" - name: Install pinned mise and Zig - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -66,9 +78,9 @@ jobs: test "$(jq -r .project_version "manifest-check/$root/manifest.json")" = "${GITHUB_REF_NAME#v}" - name: Upload validated bundle - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: grpc-lite-transpiled-c-release + name: grpc-lite-transpiled-c-release-${{ matrix.arch }} path: .zig-cache/transpiled-c-dist/* if-no-files-found: error include-hidden-files: true @@ -79,8 +91,20 @@ jobs: needs: package permissions: contents: read - runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + target: x86_64-linux-gnu + - arch: arm64 + runner: ubuntu-24.04-arm + target: aarch64-linux-gnu + runs-on: ${{ matrix.runner }} timeout-minutes: 45 + env: + GRPC_LITE_TRANSPILED_C_TARGET: ${{ matrix.target }} steps: - name: Checkout event commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -94,9 +118,9 @@ jobs: docker version - name: Download validated bundle - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - name: grpc-lite-transpiled-c-release + name: grpc-lite-transpiled-c-release-${{ matrix.arch }} path: bundle-dist - name: Test exact bundle @@ -118,9 +142,10 @@ jobs: timeout-minutes: 10 steps: - name: Download validated bundle - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - name: grpc-lite-transpiled-c-release + pattern: grpc-lite-transpiled-c-release-* + merge-multiple: true path: bundle-dist - name: Create, verify, and publish workflow draft @@ -132,12 +157,14 @@ jobs: marker='' tag=$GITHUB_REF_NAME archives=(bundle-dist/*.tar.gz) - test "${#archives[@]}" -eq 1 - archive=${archives[0]} - root=$(basename "$archive" .tar.gz) - manifest_commit=$(tar -xOf "$archive" "$root/manifest.json" | jq -r .source_commit) - test "$manifest_commit" = "$GITHUB_SHA" - (cd bundle-dist && sha256sum --check "$(basename "$archive").sha256") + test "${#archives[@]}" -eq 2 + manifest_commit=$GITHUB_SHA + for arch in x86_64 arm64; do + archive="bundle-dist/grpc-lite-transpiled-c-${tag#v}-linux-$arch.tar.gz" + root=$(basename "$archive" .tar.gz) + test "$(tar -xOf "$archive" "$root/manifest.json" | jq -r .source_commit)" = "$manifest_commit" + (cd bundle-dist && sha256sum --check "$(basename "$archive").sha256") + done require_current_tag() { local object_json object_type object_sha @@ -161,7 +188,7 @@ jobs: require_current_tag gh release create "$tag" --verify-tag --draft --title "$tag" \ --notes "$marker - Generated-only C bundle for insecure Linux x86_64." + Generated-only C bundle for insecure Linux x86_64 and ARM64." release_json=$(gh release view "$tag" --json isDraft,body) fi is_draft=$(jq -r .isDraft <<<"$release_json") diff --git a/tandem/vendor/grpc-lite/.github/workflows/repeated-tests.yml b/tandem/vendor/grpc-lite/.github/workflows/repeated-tests.yml index 50f6746f3..a053224f0 100644 --- a/tandem/vendor/grpc-lite/.github/workflows/repeated-tests.yml +++ b/tandem/vendor/grpc-lite/.github/workflows/repeated-tests.yml @@ -46,7 +46,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 diff --git a/tandem/vendor/grpc-lite/.github/workflows/soak.yml b/tandem/vendor/grpc-lite/.github/workflows/soak.yml index 03df531f6..91bf12f51 100644 --- a/tandem/vendor/grpc-lite/.github/workflows/soak.yml +++ b/tandem/vendor/grpc-lite/.github/workflows/soak.yml @@ -39,7 +39,7 @@ jobs: persist-credentials: false - name: Install mise tools - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: version: 2026.7.14 @@ -54,7 +54,7 @@ jobs: run: mise run interop-official - name: Upload soak logs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: name: official-unary-soak diff --git a/tandem/vendor/grpc-lite/AGENTS.md b/tandem/vendor/grpc-lite/AGENTS.md index e2c825c32..2b619f35a 100644 --- a/tandem/vendor/grpc-lite/AGENTS.md +++ b/tandem/vendor/grpc-lite/AGENTS.md @@ -49,7 +49,7 @@ mise run gen-grpcpp - gRPC payloads remain raw protobuf wire bytes. - Generated protobuf sources live under `.zig-cache` and are not committed. - The native Zig transport supports raw unary and full-duplex streaming over cleartext or TLS IPv4. -- The insecure Linux x86_64 transpiled-C release bundle supports cleartext transport only. +- The insecure Linux x86_64 and ARM64 transpiled-C release bundle supports cleartext transport only. - c-ares resolves client hostnames to IPv4 candidates on the libxev loop. Keep C types private to transport modules. Keep protobuf out of `src/root.zig` so the @@ -79,8 +79,9 @@ authoritative: change it before implementing a feature with a different decision | Graceful server drain | Selected | Stop admission, send GOAWAY, wait with timeout | | Server resource limits | Selected | Aggregate connection cap, per-connection stream cap, absolute cleartext preface timeout, and idle timeout only while no inbound RPC streams are active | | Native Zig TLS and ALPN | Selected | Optional mbedTLS; TLS 1.2+, mandatory `h2`, explicit PEM credentials | -| Transpiled-C release bundle TLS | Out of scope | The insecure Linux x86_64 bundle remains cleartext-only until TLS is separately selected | -| mTLS | Out of scope | Client certificates and server-side client verification are not required | +| Transpiled-C release bundles | Selected | Target-specific insecure Linux x86_64 and ARM64 packages; native CI packaging and no-Zig consumers | +| Transpiled-C release bundle TLS | Out of scope | The insecure Linux x86_64 and ARM64 bundles remain cleartext-only until TLS is separately selected | +| mTLS | Selected | Native Zig TLS only: optional client PEM identity and server-required client verification with explicit trusted CAs; no identity authorization or dynamic rotation | | DNS | Selected | c-ares on Linux io_uring; client IPv4 hostnames only | | IPv6 | Out of scope | No AAAA, Happy Eyeballs, or IPv6 listeners | | Reflection and health services | Out of scope | Not part of the lite profile | diff --git a/tandem/vendor/grpc-lite/CMakeLists.txt b/tandem/vendor/grpc-lite/CMakeLists.txt index 9d1744620..fcd519f24 100644 --- a/tandem/vendor/grpc-lite/CMakeLists.txt +++ b/tandem/vendor/grpc-lite/CMakeLists.txt @@ -1,11 +1,17 @@ cmake_minimum_required(VERSION 3.20) -project(grpc_lite VERSION 0.4.0 LANGUAGES C) +project(grpc_lite VERSION 0.5.0 LANGUAGES C) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") message(FATAL_ERROR "grpc-lite currently supports Linux only") endif() -if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") - message(FATAL_ERROR "The grpc-lite transpiled C bundle supports x86_64 only") +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") + set(_grpc_lite_target "x86_64-linux-gnu") + set(_grpc_lite_arch "x86_64") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") + set(_grpc_lite_target "aarch64-linux-gnu") + set(_grpc_lite_arch "arm64") +else() + message(FATAL_ERROR "The grpc-lite transpiled C bundle supports x86_64 and ARM64 only") endif() option(GRPC_LITE_ENABLE_TLS "Request TLS (out of scope for the transpiled-C release bundle)" OFF) @@ -20,13 +26,13 @@ set(GRPC_LITE_CARES_URL_HASH "" CACHE STRING "Optional c-ares tarball hash (ALGO if(GRPC_LITE_ENABLE_TLS) message(FATAL_ERROR - "TLS is out of scope for the insecure Linux x86_64 transpiled-C release bundle; configure with GRPC_LITE_ENABLE_TLS=OFF") + "TLS is out of scope for the insecure Linux transpiled-C release bundle; configure with GRPC_LITE_ENABLE_TLS=OFF") endif() set(_grpc_lite_bundle_help "Git checkouts and GitHub automatic 'Source code' archives intentionally omit generated C. " "Either (1) download, verify, and extract the matching " - "grpc-lite-transpiled-c-${PROJECT_VERSION}-linux-x86_64.tar.gz release asset and pass " + "grpc-lite-transpiled-c-${PROJECT_VERSION}-linux-${_grpc_lite_arch}.tar.gz release asset and pass " "-DGRPC_LITE_TRANSPILED_C_DIR=, or (2) install the pinned Zig toolchain, " "run 'mise run transpile-c', and explicitly pass that resulting directory.") if(NOT GRPC_LITE_TRANSPILED_C_DIR) @@ -54,9 +60,9 @@ endif() string(JSON _grpc_lite_manifest_target ERROR_VARIABLE _grpc_lite_json_error GET "${_grpc_lite_manifest_json}" target) if(NOT _grpc_lite_json_error STREQUAL "NOTFOUND" OR - NOT _grpc_lite_manifest_target STREQUAL "x86_64-linux-gnu") + NOT _grpc_lite_manifest_target STREQUAL "${_grpc_lite_target}") message(FATAL_ERROR - "Transpiled C bundle target mismatch: expected x86_64-linux-gnu, got '${_grpc_lite_manifest_target}'") + "Transpiled C bundle target mismatch: expected ${_grpc_lite_target}, got '${_grpc_lite_manifest_target}'") endif() set(_grpc_lite_runtime_source "${_grpc_lite_transpiled_dir}/src/grpc_lite.c") set(_grpc_lite_plugin_source "${_grpc_lite_transpiled_dir}/src/protoc-gen-grpc_lite_cpp.c") diff --git a/tandem/vendor/grpc-lite/README.md b/tandem/vendor/grpc-lite/README.md index b5c2c47d8..45aa8db3b 100644 --- a/tandem/vendor/grpc-lite/README.md +++ b/tandem/vendor/grpc-lite/README.md @@ -61,11 +61,13 @@ defer result.deinit(); Git checkouts and GitHub's automatic “Source code” archives intentionally omit generated C. To build with CMake without Zig, download the same-version -`grpc-lite-transpiled-c-0.4.0-linux-x86_64.tar.gz` release asset and its checksum, verify +`grpc-lite-transpiled-c-0.5.0-linux-x86_64.tar.gz` release asset and its checksum, verify and extract it, then configure the source tree with `-DGRPC_LITE_TRANSPILED_C_DIR=`. -The generated-only asset supports insecure Linux x86_64 and contains no third-party +Use the `linux-arm64` asset instead on Linux ARM64. + +The generated-only asset supports insecure Linux x86_64 and ARM64 and contains no third-party dependencies. TLS is out of scope for this transpiled-C release bundle until separately selected; the CMake source build rejects `GRPC_LITE_ENABLE_TLS=ON`. This bundle decision does not affect optional TLS in the native Zig build. A fully offline configure must also @@ -95,7 +97,8 @@ official grpc-go peers and public HTTP/2 framing suites on x86_64 and arm64. | grpcpp-shaped synchronous common API | Selected subset | | Official Protobuf C++ runtime | Optional external typed-code dependency | | General Abseil compatibility | Out of scope | -| IPv6, mTLS, retries, reflection, xDS | Out of scope | +| Native Zig mTLS client identities and required server-side client verification | Optional | +| IPv6, retries, reflection, xDS | Out of scope | See [the official interoperability profile](tests/official/README.md) for current cases and results. grpc-lite does not provide grpc-core ABI compatibility or the full grpcpp diff --git a/tandem/vendor/grpc-lite/build.zig b/tandem/vendor/grpc-lite/build.zig index 32e757ba5..679726028 100644 --- a/tandem/vendor/grpc-lite/build.zig +++ b/tandem/vendor/grpc-lite/build.zig @@ -154,7 +154,6 @@ pub fn build(b: *std.Build) void { grpc_lite.addOptions("grpc_lite_options", grpc_lite_options); grpc_lite.addIncludePath(nghttp2_dependency.path("lib/includes")); grpc_lite.addIncludePath(native.nghttp2_include); - grpc_lite.addIncludePath(native.nghttp2_public_include); grpc_lite.addObjectFile(native.nghttp2_archive); grpc_lite.addIncludePath(cares_dependency.path("include")); grpc_lite.addIncludePath(native.cares_include); @@ -892,7 +891,6 @@ fn addExample( const NativeDependencies = struct { nghttp2_archive: std.Build.LazyPath, nghttp2_include: std.Build.LazyPath, - nghttp2_public_include: std.Build.LazyPath, cares_archive: std.Build.LazyPath, cares_include: std.Build.LazyPath, mbedtls_archive: ?std.Build.LazyPath, @@ -1010,7 +1008,6 @@ fn addNativeDependencies( return .{ .nghttp2_archive = build_nghttp2.path(b, "lib/libnghttp2.a"), .nghttp2_include = build_nghttp2.path(b, "lib/includes"), - .nghttp2_public_include = build_nghttp2.path(b, "include"), .cares_archive = build_cares.path(b, "lib/libcares.a"), .cares_include = build_cares.path(b, "include"), .mbedtls_archive = if (build_mbedtls) |output| output.path(b, "libmbedtls_combined.a") else null, diff --git a/tandem/vendor/grpc-lite/build.zig.zon b/tandem/vendor/grpc-lite/build.zig.zon index d6f1fc16e..3b6bacda8 100644 --- a/tandem/vendor/grpc-lite/build.zig.zon +++ b/tandem/vendor/grpc-lite/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .grpc_lite, - .version = "0.4.0", + .version = "0.5.0", .minimum_zig_version = "0.16.0", .fingerprint = 0xa8480adad018cc05, .dependencies = .{ @@ -29,6 +29,16 @@ .hash = "N-V-__8AAI1FmQLj92go7nR7_J6kwr-7uYyuitngGqydvWkd", .lazy = true, }, + .protobuf = .{ + .url = "https://github.com/Arwalk/zig-protobuf/archive/b794f99323cead7f1794ae68554d0311cc309857.tar.gz", + .hash = "protobuf-5.0.0-0e82ahZiKwC5Yrh4psANoUzrV_H4CQU1EsOIY9Zdyap_", + .lazy = true, + }, + .grpc_proto = .{ + .url = "https://codeload.github.com/grpc/grpc-proto/tar.gz/99135b19189588fcc787acb84cff27991787473d", + .hash = "N-V-__8AAMvUAgBnxziu_Kuqr4AuJ00Vke6R0R-Rc1A26DRX", + .lazy = true, + }, .gperftools = .{ .url = "https://codeload.github.com/fanyang89/gperftools/tar.gz/1a01cd2cf8f7000845d343fa8e0bbac70378858b", .hash = "N-V-__8AAGKVkABvsDVHhSU8seHKvtJ8Q23b9Y0OMiFVWt-y", diff --git a/tandem/vendor/grpc-lite/cmake/grpc_liteConfigVersion.cmake b/tandem/vendor/grpc-lite/cmake/grpc_liteConfigVersion.cmake index c1f987ff9..f24616faf 100644 --- a/tandem/vendor/grpc-lite/cmake/grpc_liteConfigVersion.cmake +++ b/tandem/vendor/grpc-lite/cmake/grpc_liteConfigVersion.cmake @@ -1,4 +1,4 @@ -set(PACKAGE_VERSION "0.4.0") +set(PACKAGE_VERSION "0.5.0") if(PACKAGE_FIND_VERSION_MAJOR EQUAL 0 AND PACKAGE_FIND_VERSION VERSION_LESS_EQUAL PACKAGE_VERSION) diff --git a/tandem/vendor/grpc-lite/docs/c-cpp-api.md b/tandem/vendor/grpc-lite/docs/c-cpp-api.md index eea402873..82e9b9311 100644 --- a/tandem/vendor/grpc-lite/docs/c-cpp-api.md +++ b/tandem/vendor/grpc-lite/docs/c-cpp-api.md @@ -89,17 +89,20 @@ connection remains reusable. Git checkouts and GitHub automatic “Source code” archives intentionally contain no generated C. A no-Zig CMake build pairs the source at a tag with that release's matching -`grpc-lite-transpiled-c--linux-x86_64.tar.gz` asset. Verify its sidecar from the +`grpc-lite-transpiled-c--linux-.tar.gz` asset. Verify its sidecar from the folder containing both files, extract it, and pass the extracted root explicitly: ```bash -sha256sum --check grpc-lite-transpiled-c-0.4.0-linux-x86_64.tar.gz.sha256 -tar -xzf grpc-lite-transpiled-c-0.4.0-linux-x86_64.tar.gz +sha256sum --check grpc-lite-transpiled-c-0.5.0-linux-x86_64.tar.gz.sha256 +tar -xzf grpc-lite-transpiled-c-0.5.0-linux-x86_64.tar.gz cmake -S grpc-lite -B build \ - -DGRPC_LITE_TRANSPILED_C_DIR="$PWD/grpc-lite-transpiled-c-0.4.0-linux-x86_64" + -DGRPC_LITE_TRANSPILED_C_DIR="$PWD/grpc-lite-transpiled-c-0.5.0-linux-x86_64" ``` -The generated-only asset targets insecure Linux x86_64. TLS is out of scope for this +Choose `linux-x86_64` or `linux-arm64` to match the consumer architecture; the examples +above use x86_64. CMake rejects bundles generated for a different architecture. + +The generated-only asset targets insecure Linux x86_64 and ARM64. TLS is out of scope for this transpiled-C release bundle until separately selected, and CMake rejects `GRPC_LITE_ENABLE_TLS=ON`. This does not change the optional mbedTLS support available to native Zig builds. @@ -133,7 +136,7 @@ network guard: ```bash cmake -S grpc-lite -B build \ - -DGRPC_LITE_TRANSPILED_C_DIR="$PWD/grpc-lite-transpiled-c-0.4.0-linux-x86_64" \ + -DGRPC_LITE_TRANSPILED_C_DIR="$PWD/grpc-lite-transpiled-c-0.5.0-linux-x86_64" \ -DGRPC_LITE_NO_NETWORK=ON \ -DGRPC_LITE_NGHTTP2_URL="file://$PWD/deps/nghttp2.tar.gz" \ -DGRPC_LITE_NGHTTP2_URL_HASH=SHA256=... \ @@ -244,3 +247,20 @@ grpc-lite does not expose Abseil in its selected grpcpp-shaped API and does not substitute `absl/*` headers. Abseil used internally by a chosen Protobuf release remains that package's transitive dependency. Applications that include Abseil directly must continue to provide it or migrate those call sites separately. + +## Sanitizing transpiled-C consumers + +The Zig C backend reconstructs imported C types (including opaque pointers and +nghttp2 callback types). Clang's UBSan `function` check cannot match those +reconstructed types to the original C declarations. For a sanitized CMake +consumer, compile the C runtime and its C dependencies with +`-fsanitize=address,undefined -fno-sanitize=function`; C++ consumer code can keep +`-fsanitize=address,undefined` without that exclusion. Link with the sanitizer +flags as well. + +Zig's thread startup installs its own alternate signal stack. Run ASan consumers +with `ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:use_sigaltstack=0` to avoid ASan +trying to unmap that stack at pthread teardown. This disables ASan's alternate +signal stack, not address or leak detection. Use `UBSAN_OPTIONS=halt_on_error=1` +to fail on the remaining undefined-behavior checks. These settings are validation +constraints, not a claim of full function-type sanitizer coverage. diff --git a/tandem/vendor/grpc-lite/docs/development.md b/tandem/vendor/grpc-lite/docs/development.md index e2eb808fe..35c7e2404 100644 --- a/tandem/vendor/grpc-lite/docs/development.md +++ b/tandem/vendor/grpc-lite/docs/development.md @@ -67,18 +67,26 @@ The authoritative compatibility matrix lives in `mise run transpile-c` emits the runtime and C++ protoc plugin through Zig's C backend, vendors the matching `zig.h`, writes a manifest, and applies deterministic compatibility -transforms. The ignored `transpiled/` directory is for local use and is never committed. +transforms. The default target follows the host architecture. Set +`GRPC_LITE_TRANSPILED_C_TARGET=x86_64-linux-gnu` or `aarch64-linux-gnu` to override +it for `transpile-c`, `package-transpiled-c`, or `check-transpiled-c`. Generated C is +target-specific; it is not portable between architectures. Package suffixes are +`linux-x86_64` and `linux-arm64`. Use separate output directories when preserving +both architectures (`GRPC_LITE_TRANSPILED_C_DIST_DIR`), since the reproducibility +check clears its output directory. + +The ignored `transpiled/` directory is for local use and is never committed. `mise run check-transpiled-c` generates and packages twice with independent caches, compares the resulting archives byte for byte, verifies the SHA-256 sidecar, and tests that neither tracked files nor `git archive` contain `transpiled/`. The generated-only -release asset is named `grpc-lite-transpiled-c--linux-x86_64.tar.gz` and contains +release asset is named `grpc-lite-transpiled-c--linux-.tar.gz` and contains only the manifest, two C translation units, and `zig.h`. -CI consumes the exact archive in a separate environment where Zig is absent and the +CI and tag releases package both architectures on native runners and consume the exact archive in a separate environment where Zig is absent and the network is disabled. Git checkouts and GitHub automatic “Source code” archives do not support no-Zig CMake by themselves: consumers must extract the same-version asset and -pass `GRPC_LITE_TRANSPILED_C_DIR`. The asset is insecure Linux x86_64 only and includes +pass `GRPC_LITE_TRANSPILED_C_DIR`. The assets support insecure Linux x86_64 and ARM64 and includes no nghttp2 or c-ares sources. TLS is out of scope for this transpiled-C release bundle until separately selected, so CMake rejects `GRPC_LITE_ENABLE_TLS=ON`. Native Zig TLS remains selected and is validated by the separate TLS tasks above. Fully offline builds diff --git a/tandem/vendor/grpc-lite/docs/zig-api.md b/tandem/vendor/grpc-lite/docs/zig-api.md index 05b3384d3..42abe3268 100644 --- a/tandem/vendor/grpc-lite/docs/zig-api.md +++ b/tandem/vendor/grpc-lite/docs/zig-api.md @@ -192,6 +192,40 @@ var channel = try grpc.Channel.init(allocator, "api.example.com:443", .{ }); ``` +For mutual TLS, a client presents a certificate chain and unencrypted PEM private key +when the server requests one. The key must match the leaf certificate, otherwise +`Channel.init` fails with `error.CertificateKeyMismatch`. PEM input is parsed during +`Channel.init` and may be released afterward. + +```zig +var channel = try grpc.Channel.init(allocator, "api.example.com:443", .{ + .runtime = &runtime, + .tls = .{ + .ca_certificates_pem = ca_pem, + .identity = .{ .certificate_chain_pem = client_cert_pem, .private_key_pem = client_key_pem }, + }, +}); +``` + +A server can require client certificates by setting `client_ca_certificates_pem`: + +```zig +var server = try grpc.Server.init(allocator, .{ + .tls = .{ + .certificate_chain_pem = server_cert_pem, + .private_key_pem = server_key_pem, + .client_ca_certificates_pem = client_ca_pem, + }, +}); +``` + +The default (`null`) does not request a client certificate. When configured, a +missing, expired, or untrusted client certificate fails the handshake. An empty or +invalid CA PEM fails initialization; it does not disable verification. CA PEM is +parsed during `Server.init` and may then be released. Client and server trust roots +may differ. This authenticates certificate trust only, not per-RPC authorization; +peer identity APIs, dynamic credential rotation, and C/C++ TLS are not provided. + Servers accept a PEM certificate chain and unencrypted PEM private key. PEM input is parsed during `Server.init` and may be released afterward. diff --git a/tandem/vendor/grpc-lite/src/c_api.zig b/tandem/vendor/grpc-lite/src/c_api.zig index ba7878d4b..aa98fd509 100644 --- a/tandem/vendor/grpc-lite/src/c_api.zig +++ b/tandem/vendor/grpc-lite/src/c_api.zig @@ -181,7 +181,7 @@ const channel_options_v1_size = @offsetOf(ChannelOptions, "jitter_percent") + @s const channel_options_v2_size = @offsetOf(ChannelOptions, "logger") + @sizeOf(?*const Logger); const channel_options_v3_size = @offsetOf(ChannelOptions, "max_response_header_list_size") + @sizeOf(u64); -const allocator = std.heap.c_allocator; +const allocator = if (@import("builtin").is_test) std.testing.allocator else std.heap.c_allocator; const ChannelCreateOptions = struct { reconnect: ?channel.ReconnectOptions = null, @@ -351,7 +351,8 @@ fn createChannel( output.* = null; const target_bytes = bytes(target) orelse return .invalid_argument; const storage = allocator.create(ChannelStorage) catch return .out_of_memory; - errdefer allocator.destroy(storage); + // Error is a C status enum, not a Zig error union: errdefer would not run. + defer if (output.* == null) allocator.destroy(storage); storage.value = channel.Channel.init(allocator, target_bytes, .{ .runtime = if (runtime_handle) |handle| &runtimeStorage(handle).value else null, .reconnect = options.reconnect, @@ -399,7 +400,8 @@ pub fn grpc_lite_channel_call_unary( const options = parseUnaryOptions(options_pointer) orelse return .invalid_argument; const storage = allocator.create(UnaryResultStorage) catch return .out_of_memory; - errdefer allocator.destroy(storage); + // Error is a C status enum, not a Zig error union: errdefer would not run. + defer if (output.* == null) allocator.destroy(storage); storage.value = channelStorage(handle).value.callUnary( allocator, method, @@ -429,7 +431,8 @@ pub fn grpc_lite_channel_open_stream( const callbacks = parseClientStreamCallbacks(callbacks_pointer) orelse return .invalid_argument; const storage = allocator.create(ClientStreamStorage) catch return .out_of_memory; - errdefer allocator.destroy(storage); + // Error is a C status enum, not a Zig error union: errdefer would not run. + defer if (output.* == null) allocator.destroy(storage); storage.callbacks = callbacks; storage.ready = .init(false); storage.value = channelStorage(handle).value.openStream(method, options, .{ @@ -512,7 +515,6 @@ pub fn grpc_lite_server_register_stream( const callbacks = parseServerMethodCallbacks(callbacks_pointer) orelse return .invalid_argument; const server_storage = serverStorage(handle); const method = allocator.create(ServerMethodStorage) catch return .out_of_memory; - errdefer allocator.destroy(method); method.* = .{ .callbacks = callbacks }; server_storage.methods.ensureUnusedCapacity(allocator, 1) catch { allocator.destroy(method); @@ -1774,3 +1776,28 @@ test "C client stream completes an event-driven round trip" { try std.testing.expect(state.message_seen.load(.acquire)); try std.testing.expect(state.succeeded.load(.acquire)); } + +test "C failed unary and stream creation release handle storage" { + const Callbacks = struct { + fn onMessage(_: ?*anyopaque, _: *ClientStreamHandle, _: BytesView, _: u32) callconv(.c) u32 { + return 0; + } + fn onTerminal(_: ?*anyopaque, _: *ClientStreamHandle, _: i32, _: BytesView, _: *const MetadataViewHandle) callconv(.c) void {} + }; + const options: ChannelOptions = .{ .allow_initial_offline = 1 }; + var handle: ?*ChannelHandle = null; + try std.testing.expectEqual(Error.ok, grpc_lite_channel_create_managed(null, view("127.0.0.1:1"), &options, &handle)); + defer grpc_lite_channel_destroy(handle); + grpc_lite_channel_shutdown(handle); + grpc_lite_channel_wait(handle); + + var result: ?*UnaryResultHandle = @ptrFromInt(1); + try std.testing.expectEqual(Error.invalid_argument, grpc_lite_channel_call_unary(handle, view("invalid"), view(""), null, &result)); + try std.testing.expectEqual(null, result); + const callbacks: ClientStreamCallbacks = .{ .on_message = Callbacks.onMessage, .on_terminal = Callbacks.onTerminal }; + var stream: ?*ClientStreamHandle = @ptrFromInt(1); + try std.testing.expectEqual(Error.invalid_argument, grpc_lite_channel_open_stream(handle, view("invalid"), null, &callbacks, &stream)); + try std.testing.expectEqual(null, stream); + try std.testing.expect(grpc_lite_channel_open_stream(handle, view("/test.Echo/Stream"), null, &callbacks, &stream) != .ok); + try std.testing.expectEqual(null, stream); +} diff --git a/tandem/vendor/grpc-lite/src/channel.zig b/tandem/vendor/grpc-lite/src/channel.zig index b654a3af2..9f78ccce1 100644 --- a/tandem/vendor/grpc-lite/src/channel.zig +++ b/tandem/vendor/grpc-lite/src/channel.zig @@ -66,9 +66,17 @@ threadlocal var receiving_http2_impl: ?*Impl = null; pub const TlsOptions = struct { ca_certificates_pem: []const u8, + /// Client certificate for mutual TLS, presented when the server requests one. + identity: ?TlsIdentity = null, handshake_timeout_ns: u64 = 10 * std.time.ns_per_s, }; +/// PEM certificate chain and unencrypted PEM private key; parsed during `Channel.init`. +pub const TlsIdentity = struct { + certificate_chain_pem: []const u8, + private_key_pem: []const u8, +}; + pub const ReconnectOptions = struct { allow_initial_offline: bool = false, initial_backoff_ns: u64 = std.time.ns_per_s, @@ -243,6 +251,10 @@ pub const Channel = struct { impl.tls_config = try tls_record.Config.createClient( impl.allocator, tls_options.ca_certificates_pem, + if (tls_options.identity) |identity| .{ + .certificate_chain_pem = identity.certificate_chain_pem, + .private_key_pem = identity.private_key_pem, + } else null, ); } } @@ -7865,6 +7877,58 @@ test "channel and server exchange a unary call over TLS" { try std.testing.expectEqualStrings("secure", result.payload); } +test "channel and server exchange a unary call over mutual TLS" { + if (!build_options.tls) return error.SkipZigTest; + const server = @import("server.zig"); + const service = @import("service.zig"); + const certificate = @embedFile("testdata/localhost-cert.pem"); + const private_key = @embedFile("testdata/localhost-key.pem"); + + const Handler = struct { + fn handle( + _: *@This(), + allocator: std.mem.Allocator, + _: *service.ServerContext, + request: []const u8, + ) !service.UnaryResponse { + return service.UnaryResponse.ok(allocator, request); + } + }; + + var handler = Handler{}; + var test_server = try server.Server.init(std.testing.allocator, .{ + .tls = .{ + .certificate_chain_pem = certificate, + .private_key_pem = private_key, + .client_ca_certificates_pem = @embedFile("testdata/mtls/ca.pem"), + }, + }); + defer test_server.deinit(); + try test_server.registerUnary( + "/test.Tls/Unary", + service.UnaryHandler.bind(Handler, &handler, Handler.handle), + ); + try test_server.start(); + + var runtime = try Runtime.init(); + defer runtime.deinit(); + var target_buffer: [32]u8 = undefined; + const target = try std.fmt.bufPrint(&target_buffer, "localhost:{d}", .{try test_server.port()}); + var channel = try Channel.init(std.testing.allocator, target, .{ + .runtime = &runtime, + .tls = .{ .ca_certificates_pem = certificate, .identity = .{ + .certificate_chain_pem = @embedFile("testdata/mtls/client.pem"), + .private_key_pem = @embedFile("testdata/mtls/client-key.pem"), + } }, + }); + defer channel.deinit(); + + var result = try channel.callUnary(std.testing.allocator, "/test.Tls/Unary", "secure", .{}); + defer result.deinit(); + try std.testing.expect(result.status.isOk()); + try std.testing.expectEqualStrings("secure", result.payload); +} + test "TLS channel replaces a server-idle connection for a later RPC" { if (!build_options.tls) return error.SkipZigTest; const server = @import("server.zig"); @@ -7897,7 +7961,7 @@ test "TLS channel replaces a server-idle connection for a later RPC" { fn dump(self: *@This(), err: anyerror) void { self.mutex.lockUncancelable(syncIo()); defer self.mutex.unlock(syncIo()); - std.debug.print("TLS idle replacement failed: {s} (idle=20ms sleep=100ms high=16 low=8 dropped={d})\n{s}", .{ + std.debug.print("TLS idle replacement failed: {s} (idle=1000ms replacement_wait=5000ms high=16 low=8 dropped={d})\n{s}", .{ @errorName(err), self.dropped, self.buffer[0..self.len], }); } @@ -7922,7 +7986,7 @@ test "TLS channel replaces a server-idle connection for a later RPC" { var handler = Handler{}; var test_server = try server.Server.init(std.testing.allocator, .{ .logger = logger, - .connection_idle_timeout_ns = 20 * std.time.ns_per_ms, + .connection_idle_timeout_ns = std.time.ns_per_s, .write_high_watermark_bytes = 16, .write_low_watermark_bytes = 8, .tls = .{ @@ -7949,14 +8013,26 @@ test "TLS channel replaces a server-idle connection for a later RPC" { }); defer channel.deinit(); - logger.write(.debug, "test: first RPC start generation={d}", .{channel.impl.connection_generation.load(.monotonic)}); + const first_generation = channel.impl.connection_generation.load(.monotonic); + logger.write(.debug, "test: first RPC start generation={d}", .{first_generation}); var first = try channel.callUnary(std.testing.allocator, "/test.Tls/Unary", "first", .{}); defer first.deinit(); logger.write(.debug, "test: first RPC status={s} message={s} generation={d}", .{ @tagName(first.status.code), first.status.message, channel.impl.connection_generation.load(.monotonic), }); try std.testing.expect(first.status.isOk()); - try std.Io.sleep(std.testing.io, .fromMilliseconds(100), .awake); + // A fixed sleep can span several idle replacements and race the next close. + // Generation advances before the TLS handshake, so also wait for admission. + const replacement_deadline = nowNs() +| 5 * std.time.ns_per_s; + while (true) { + channel.impl.mutex.lockUncancelable(syncIo()); + const replacement_ready = channel.impl.connection_generation.load(.monotonic) > first_generation and + channel.impl.accepting_streams; + channel.impl.mutex.unlock(syncIo()); + if (replacement_ready) break; + try std.testing.expect(nowNs() < replacement_deadline); + try std.Io.sleep(std.testing.io, .fromMilliseconds(1), .awake); + } logger.write(.debug, "test: second RPC start generation={d}", .{channel.impl.connection_generation.load(.monotonic)}); var second = try channel.callUnary(std.testing.allocator, "/test.Tls/Unary", "second", .{}); defer second.deinit(); diff --git a/tandem/vendor/grpc-lite/src/cpucycles_compat.c b/tandem/vendor/grpc-lite/src/cpucycles_compat.c index 62ea34481..3b51fdd0e 100644 --- a/tandem/vendor/grpc-lite/src/cpucycles_compat.c +++ b/tandem/vendor/grpc-lite/src/cpucycles_compat.c @@ -1,13 +1,13 @@ -#include #include -static int64_t cpucycles_monotonic(void) { +/* Match the libcpucycles function-pointer ABI; int64_t can be long. */ +static long long cpucycles_monotonic(void) { struct timespec value; if (clock_gettime(CLOCK_MONOTONIC, &value) != 0) return -1; - return (int64_t)value.tv_sec * 1000000000 + value.tv_nsec; + return (long long)value.tv_sec * 1000000000LL + value.tv_nsec; } -int64_t (*cpucycles)(void) = cpucycles_monotonic; +long long (*cpucycles)(void) = cpucycles_monotonic; const char *cpucycles_implementation(void) { return "default-monotonic"; diff --git a/tandem/vendor/grpc-lite/src/root.zig b/tandem/vendor/grpc-lite/src/root.zig index bf9efca77..5c2276484 100644 --- a/tandem/vendor/grpc-lite/src/root.zig +++ b/tandem/vendor/grpc-lite/src/root.zig @@ -94,6 +94,7 @@ pub const Channel = channel.Channel; pub const ChannelOptions = channel.Options; pub const ReconnectOptions = channel.ReconnectOptions; pub const ClientTlsOptions = channel.TlsOptions; +pub const ClientTlsIdentity = channel.TlsIdentity; pub const Metadata = metadata.Metadata; pub const MetadataEntry = metadata.Entry; pub const Server = server.Server; diff --git a/tandem/vendor/grpc-lite/src/server.zig b/tandem/vendor/grpc-lite/src/server.zig index 29b28e9f5..90c9e4ca1 100644 --- a/tandem/vendor/grpc-lite/src/server.zig +++ b/tandem/vendor/grpc-lite/src/server.zig @@ -208,8 +208,8 @@ const EncodedValueBuilder = StackFirstBuilder(metadata.OutboundValue, encoded_va pub const TlsOptions = struct { certificate_chain_pem: []const u8, private_key_pem: []const u8, - ca_certificates_pem: ?[]const u8 = null, - require_client_certificate: bool = false, + /// When set, require a valid client certificate signed by these PEM CAs. + client_ca_certificates_pem: ?[]const u8 = null, handshake_timeout_ns: u64 = 10 * std.time.ns_per_s, }; @@ -800,8 +800,7 @@ fn initImpl(shared_allocator: std.mem.Allocator, coordinator: *Coordinator, opti shared_allocator, tls_options.certificate_chain_pem, tls_options.private_key_pem, - tls_options.ca_certificates_pem, - tls_options.require_client_certificate, + tls_options.client_ca_certificates_pem, ); } } @@ -5193,8 +5192,10 @@ test "server resource silent and incomplete cleartext prefaces expire" { test "server resource fragmented valid cleartext preface survives into idle" { var server = try Server.init(std.testing.allocator, .{ - .cleartext_preface_timeout_ns = 40 * std.time.ns_per_ms, - .connection_idle_timeout_ns = std.time.ns_per_s, + // Allow instrumentation and scheduler overhead while keeping the preface + // deadline shorter than the observation interval and the idle deadline. + .cleartext_preface_timeout_ns = std.time.ns_per_s, + .connection_idle_timeout_ns = 30 * std.time.ns_per_s, }); defer server.deinit(); try server.start(); @@ -5206,6 +5207,7 @@ test "server resource fragmented valid cleartext preface survives into idle" { const io = io_threaded.io(); const stream = try address.connect(io, .{ .mode = .stream, .timeout = .none }); defer stream.close(io); + try socket_options.enableTcpNoDelay(stream.socket.handle); const preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" ++ [_]u8{ 0, 0, 0, c.NGHTTP2_SETTINGS, 0, 0, 0, 0, 0 }; var offset: usize = 0; while (offset < preface.len) { @@ -5213,8 +5215,26 @@ test "server resource fragmented valid cleartext preface survives into idle" { try writeTestStream(stream, io, preface[offset..end]); offset = end; } - try waitForAdmittedConnections(server.coordinator, 1); - try std.Io.sleep(std.testing.io, .fromMilliseconds(80), .awake); + // Admission only proves accept() ran. Wait for the SETTINGS ACK to prove + // that the complete fragmented preface was processed before observing idle. + var output: std.ArrayList(u8) = .empty; + defer output.deinit(std.testing.allocator); + var buffer: [256]u8 = undefined; + for (0..10) |_| { + var poll_fds = [_]std.posix.pollfd{.{ + .fd = stream.socket.handle, + .events = std.posix.POLL.IN, + .revents = 0, + }}; + if (try std.posix.poll(&poll_fds, 1000) == 0) return error.TestTimeout; + const length = try std.posix.read(stream.socket.handle, &buffer); + if (length == 0) return error.UnexpectedEof; + try output.appendSlice(std.testing.allocator, buffer[0..length]); + if (testOutputHasFrame(output.items, 0, c.NGHTTP2_SETTINGS, c.NGHTTP2_FLAG_ACK)) break; + } + try std.testing.expect(testOutputHasFrame(output.items, 0, c.NGHTTP2_SETTINGS, c.NGHTTP2_FLAG_ACK)); + // A stale preface deadline must still close the connection and fail this test. + try std.Io.sleep(std.testing.io, .fromMilliseconds(1100), .awake); try std.testing.expectEqual(@as(usize, 1), server.coordinator.admitted_connections.load(.acquire)); } diff --git a/tandem/vendor/grpc-lite/src/testdata/mtls/ca.pem b/tandem/vendor/grpc-lite/src/testdata/mtls/ca.pem new file mode 100644 index 000000000..3d798741f --- /dev/null +++ b/tandem/vendor/grpc-lite/src/testdata/mtls/ca.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBkjCCATegAwIBAgIUEI4GPX+34XbSzCYWlLCb6+F30U0wCgYIKoZIzj0EAwIw +HjEcMBoGA1UEAwwTZ3JwYy1saXRlLWNsaWVudC1jYTAeFw0yNjEwMTUwODA2MzZa +Fw00NjEwMTAwODA2MzZaMB4xHDAaBgNVBAMME2dycGMtbGl0ZS1jbGllbnQtY2Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARNJp1YY3ksvuV2P0uuJP+os1XLr8Bo +WrQ0kdP273G9oPzXnnCv5zAzk1KmMjmN+TjJtwu1QUhZpYVSNgwsAizdo1MwUTAd +BgNVHQ4EFgQUNAQ3K+jXeSSBbaRDMn6gnLrT9hswHwYDVR0jBBgwFoAUNAQ3K+jX +eSSBbaRDMn6gnLrT9hswDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNJADBG +AiEAwDF9a5Z+IiG3McyRg8kNu4rVGFHR7GrF9c+jeEGhT/oCIQDOfyBF6sBT+EK4 +lu+8ivu62byzjTXOLQP+ln1WanKMPA== +-----END CERTIFICATE----- diff --git a/tandem/vendor/grpc-lite/src/testdata/mtls/client-key.pem b/tandem/vendor/grpc-lite/src/testdata/mtls/client-key.pem new file mode 100644 index 000000000..789633b70 --- /dev/null +++ b/tandem/vendor/grpc-lite/src/testdata/mtls/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJhzMDeiMVH+S/dIv4fNpXIxqb9H0hFdAbo35kIeL8GIoAoGCCqGSM49 +AwEHoUQDQgAEFrgngBMVLR8A1uNB/9uJQAbA5sFPA8Y/Lc38b4kgT23VgINNHtQh +gaFET3AxHh6p71uXQwJG9d6Z4cL4/mslxA== +-----END EC PRIVATE KEY----- diff --git a/tandem/vendor/grpc-lite/src/testdata/mtls/client.pem b/tandem/vendor/grpc-lite/src/testdata/mtls/client.pem new file mode 100644 index 000000000..2475e1346 --- /dev/null +++ b/tandem/vendor/grpc-lite/src/testdata/mtls/client.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBnTCCAUOgAwIBAgIBATAKBggqhkjOPQQDAjAeMRwwGgYDVQQDDBNncnBjLWxp +dGUtY2xpZW50LWNhMB4XDTIwMDEwMTAwMDAwMFoXDTQ2MDEwMTAwMDAwMFowGzEZ +MBcGA1UEAwwQZ3JwYy1saXRlLWNsaWVudDBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABBa4J4ATFS0fANbjQf/biUAGwObBTwPGPy3N/G+JIE9t1YCDTR7UIYGhRE9w +MR4eqe9bl0MCRvXemeHC+P5rJcSjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/ +BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBR2ZyLs6OPu7bKW +7MuntZP1C2/8vjAfBgNVHSMEGDAWgBQ0BDcr6Nd5JIFtpEMyfqCcutP2GzAKBggq +hkjOPQQDAgNIADBFAiAiLgVsjBunfzIr4YzvRVAP1IbK7tWLyi1+/nKErqTjHQIh +AJWJPaPyeALFzvLOHlFUC7nH99gFM641/abBNfelmrID +-----END CERTIFICATE----- diff --git a/tandem/vendor/grpc-lite/src/testdata/mtls/expired-client.pem b/tandem/vendor/grpc-lite/src/testdata/mtls/expired-client.pem new file mode 100644 index 000000000..2e0c7eb15 --- /dev/null +++ b/tandem/vendor/grpc-lite/src/testdata/mtls/expired-client.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBnTCCAUOgAwIBAgIBAjAKBggqhkjOPQQDAjAeMRwwGgYDVQQDDBNncnBjLWxp +dGUtY2xpZW50LWNhMB4XDTIwMDEwMTAwMDAwMFoXDTIxMDEwMTAwMDAwMFowGzEZ +MBcGA1UEAwwQZ3JwYy1saXRlLWNsaWVudDBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABBa4J4ATFS0fANbjQf/biUAGwObBTwPGPy3N/G+JIE9t1YCDTR7UIYGhRE9w +MR4eqe9bl0MCRvXemeHC+P5rJcSjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/ +BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBR2ZyLs6OPu7bKW +7MuntZP1C2/8vjAfBgNVHSMEGDAWgBQ0BDcr6Nd5JIFtpEMyfqCcutP2GzAKBggq +hkjOPQQDAgNIADBFAiB9mL9E5c8vDULQXV6AAanF09fSAmdz0l8X7mDD5YW/jQIh +AMWppXq/0rQAgAHH5X5kRKUGjqTyCRELRvTInNRBN+Nq +-----END CERTIFICATE----- diff --git a/tandem/vendor/grpc-lite/src/testdata/mtls/generate.sh b/tandem/vendor/grpc-lite/src/testdata/mtls/generate.sh new file mode 100644 index 000000000..22517f224 --- /dev/null +++ b/tandem/vendor/grpc-lite/src/testdata/mtls/generate.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Test-only credentials. Never use these private keys outside tests. +set -euo pipefail +cd "$(dirname "$0")" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +openssl ecparam -name prime256v1 -genkey -noout -out ca-key.pem +openssl req -new -x509 -key ca-key.pem -subj /CN=grpc-lite-client-ca -days 7300 -out ca.pem +openssl ecparam -name prime256v1 -genkey -noout -out client-key.pem +openssl req -new -key client-key.pem -subj /CN=grpc-lite-client -out "$work/client.csr" +touch "$work/index" +echo 01 > "$work/serial" +cat > "$work/ca.cnf" < outcome.client_complete = true, + .want_read, .want_write => {}, + else => return outcome, + }; + try transferCiphertext(client, server); + if (!outcome.server_complete) switch (server.handshake()) { + .complete => outcome.server_complete = true, + .want_read, .want_write => {}, + else => { + outcome.server_failed = true; + return outcome; + }, + }; + try transferCiphertext(server, client); + if (outcome.client_complete and outcome.server_complete) break; + } + return outcome; +} + fn handshakeStep(session: *Session) !bool { return switch (session.handshake()) { .complete => true, diff --git a/tandem/vendor/grpc-lite/tests/consumer/run_cmake_c.sh b/tandem/vendor/grpc-lite/tests/consumer/run_cmake_c.sh index 21649921f..ddfed6c44 100755 --- a/tandem/vendor/grpc-lite/tests/consumer/run_cmake_c.sh +++ b/tandem/vendor/grpc-lite/tests/consumer/run_cmake_c.sh @@ -44,6 +44,22 @@ run_isolated() { fi grep -q 'bundle version mismatch' "$work_dir/negative-mismatch.log" + # The mismatched fixture is JSON emitted by this script's json.dump below. + local wrong_target=x86_64-linux-gnu + if grep -q '"target": "x86_64-linux-gnu"' "$mismatch_bundle/manifest.json"; then + wrong_target=aarch64-linux-gnu + fi + sed -i -e "s/\"project_version\": \"999.0.0\"/\"project_version\": \"$version\"/" \ + -e "s/\"target\": \"[^\"]*\"/\"target\": \"$wrong_target\"/" \ + "$mismatch_bundle/manifest.json" + if cmake -S "$package_source" -B "$work_dir/negative-target" \ + "${common[@]}" -DGRPC_LITE_TRANSPILED_C_DIR="$mismatch_bundle" \ + >"$work_dir/negative-target.log" 2>&1; then + printf '%s\n' 'CMake unexpectedly accepted a bundle for another architecture' >&2 + exit 1 + fi + grep -q 'bundle target mismatch' "$work_dir/negative-target.log" + if cmake -S "$package_source" -B "$work_dir/negative-network" \ -G Ninja -DGRPC_LITE_NO_NETWORK=ON \ -DGRPC_LITE_TRANSPILED_C_DIR="$bundle_dir" \ diff --git a/tandem/vendor/grpc-lite/tests/official/README.md b/tandem/vendor/grpc-lite/tests/official/README.md index 23e6c1a93..87259d5a1 100644 --- a/tandem/vendor/grpc-lite/tests/official/README.md +++ b/tandem/vendor/grpc-lite/tests/official/README.md @@ -58,3 +58,11 @@ wheels for Linux x86_64 and aarch64. The harness builds a minimal local image fr multi-architecture Python base pinned by manifest digest; it does not depend on the upstream interop image registry or a live grpc checkout. Docker is required for this suite. See `third_party/grpc-http2-test/UPSTREAM.md` for provenance and update rules. + +### Mutual TLS + +`mise run interop-tls` also tests native Zig client/server mutual TLS against +grpc-go: client identity presentation, required client verification, rejection of +missing/untrusted/expired identities, and unary/streaming calls to a grpc-go mTLS +server. Test-only client credentials live in `src/testdata/mtls`; regenerate them +with `bash src/testdata/mtls/generate.sh` (OpenSSL required). Never deploy these keys. diff --git a/tandem/vendor/grpc-lite/tests/official/go.mod b/tandem/vendor/grpc-lite/tests/official/go.mod index 44ed88d7c..e6ec1ff6c 100644 --- a/tandem/vendor/grpc-lite/tests/official/go.mod +++ b/tandem/vendor/grpc-lite/tests/official/go.mod @@ -5,8 +5,12 @@ go 1.26 require google.golang.org/grpc v1.82.1 require ( + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/grpc/grpc/tools/http2_interop v0.0.0-20260718051024-8542e01ff47e // indirect golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect diff --git a/tandem/vendor/grpc-lite/tests/official/interop_client.zig b/tandem/vendor/grpc-lite/tests/official/interop_client.zig index c6ad61721..1404f56d7 100644 --- a/tandem/vendor/grpc-lite/tests/official/interop_client.zig +++ b/tandem/vendor/grpc-lite/tests/official/interop_client.zig @@ -16,6 +16,8 @@ const Config = struct { server_port: u16 = 10000, test_case: []const u8 = "large_unary", use_tls: bool = false, + tls_cert_file: []const u8 = "", + tls_key_file: []const u8 = "", ca_file: []const u8 = "", soak_iterations: usize = 10, soak_max_failures: usize = 0, @@ -33,8 +35,24 @@ pub fn main(init: std.process.Init) !void { else null; defer if (ca_pem) |bytes| init.gpa.free(bytes); + const tls_cert_file_pem = if (config.tls_cert_file.len != 0) + try std.Io.Dir.cwd().readFileAlloc(init.io, config.tls_cert_file, init.gpa, .limited(4 * 1024 * 1024)) + else + null; + defer if (tls_cert_file_pem) |bytes| init.gpa.free(bytes); + const tls_key_file_pem = if (config.tls_key_file.len != 0) + try std.Io.Dir.cwd().readFileAlloc(init.io, config.tls_key_file, init.gpa, .limited(4 * 1024 * 1024)) + else + null; + defer if (tls_key_file_pem) |bytes| init.gpa.free(bytes); const channel_options: grpc.ChannelOptions = .{ - .tls = if (ca_pem) |bytes| .{ .ca_certificates_pem = bytes } else null, + .tls = if (ca_pem) |bytes| .{ + .ca_certificates_pem = bytes, + .identity = if (tls_cert_file_pem) |cert| .{ + .certificate_chain_pem = cert, + .private_key_pem = tls_key_file_pem.?, + } else null, + } else null, }; const target = try std.fmt.allocPrint(init.gpa, "{s}:{d}", .{ @@ -903,6 +921,10 @@ fn parseArgs(args: []const []const u8) !Config { index += 1; if (index >= args.len) return error.MissingTestCase; config.test_case = args[index]; + } else if (std.mem.startsWith(u8, arg, "--tls_cert_file=")) { + config.tls_cert_file = arg["--tls_cert_file=".len..]; + } else if (std.mem.startsWith(u8, arg, "--tls_key_file=")) { + config.tls_key_file = arg["--tls_key_file=".len..]; } else if (std.mem.startsWith(u8, arg, "--use_tls=")) { config.use_tls = try parseBool(arg["--use_tls=".len..]); } else if (std.mem.eql(u8, arg, "--use_tls")) { @@ -950,6 +972,8 @@ fn parseArgs(args: []const []const u8) !Config { return error.UnknownArgument; } } + if ((config.tls_cert_file.len == 0) != (config.tls_key_file.len == 0)) return error.IncompleteClientIdentity; + if (!config.use_tls and config.tls_cert_file.len != 0) return error.IdentityRequiresTls; if (config.use_tls and config.ca_file.len == 0) return error.MissingCaFile; if (config.soak_iterations == 0) return error.ZeroSoakIterations; return config; @@ -1089,3 +1113,11 @@ test "channel soak counts init failures and preserves OOM" { ); try std.testing.expectEqual(@as(usize, 1), failures); } + +test "parse mutual TLS identity arguments" { + const config = try parseArgs(&.{ "client", "--use_tls=true", "--ca_file=ca.pem", "--tls_cert_file=client.pem", "--tls_key_file=client-key.pem" }); + try std.testing.expectEqualStrings("client.pem", config.tls_cert_file); + try std.testing.expectEqualStrings("client-key.pem", config.tls_key_file); + try std.testing.expectError(error.IncompleteClientIdentity, parseArgs(&.{ "client", "--tls_cert_file=client.pem" })); + try std.testing.expectError(error.IncompleteClientIdentity, parseArgs(&.{ "client", "--tls_key_file=client-key.pem" })); +} diff --git a/tandem/vendor/grpc-lite/tests/official/interop_server.zig b/tandem/vendor/grpc-lite/tests/official/interop_server.zig index 2578fe1ed..07d0f427f 100644 --- a/tandem/vendor/grpc-lite/tests/official/interop_server.zig +++ b/tandem/vendor/grpc-lite/tests/official/interop_server.zig @@ -5,6 +5,7 @@ const testing = @import("grpc_testing"); const Config = struct { port: u16 = 10000, use_tls: bool = false, + client_ca_file: []const u8 = "", tls_cert_file: []const u8 = "", tls_key_file: []const u8 = "", }; @@ -458,6 +459,12 @@ pub fn main(init: std.process.Init) !void { null; defer if (private_key) |bytes| init.gpa.free(bytes); + const client_ca_file_pem = if (config.client_ca_file.len != 0) + try std.Io.Dir.cwd().readFileAlloc(init.io, config.client_ca_file, init.gpa, .limited(4 * 1024 * 1024)) + else + null; + defer if (client_ca_file_pem) |bytes| init.gpa.free(bytes); + var service = Service.init(init.gpa); defer service.deinit(); var server = try grpc.Server.init(init.gpa, .{ @@ -466,6 +473,7 @@ pub fn main(init: std.process.Init) !void { .tls = if (certificate) |cert| .{ .certificate_chain_pem = cert, .private_key_pem = private_key.?, + .client_ca_certificates_pem = client_ca_file_pem, } else null, }); defer server.deinit(); @@ -537,6 +545,8 @@ fn parseArgs(args: []const []const u8) !Config { index += 1; if (index >= args.len) return error.MissingPort; config.port = try std.fmt.parseInt(u16, args[index], 10); + } else if (std.mem.startsWith(u8, arg, "--client_ca_file=")) { + config.client_ca_file = arg["--client_ca_file=".len..]; } else if (std.mem.startsWith(u8, arg, "--use_tls=")) { config.use_tls = try parseBool(arg["--use_tls=".len..]); } else if (std.mem.eql(u8, arg, "--use_tls")) { @@ -557,6 +567,7 @@ fn parseArgs(args: []const []const u8) !Config { return error.UnknownArgument; } } + if (!config.use_tls and config.client_ca_file.len != 0) return error.ClientVerificationRequiresTls; if (config.use_tls and config.tls_cert_file.len == 0) return error.MissingTlsCertFile; if (config.use_tls and config.tls_key_file.len == 0) return error.MissingTlsKeyFile; return config; diff --git a/tandem/vendor/grpc-lite/tests/official/mtls_peer/main.go b/tandem/vendor/grpc-lite/tests/official/mtls_peer/main.go new file mode 100644 index 000000000..2f11ca380 --- /dev/null +++ b/tandem/vendor/grpc-lite/tests/official/mtls_peer/main.go @@ -0,0 +1,72 @@ +// A grpc-go peer for native TLS client-certificate interoperability tests. +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "net" + "os" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/interop" + pb "google.golang.org/grpc/interop/grpc_testing" +) + +func run() error { + server := flag.Bool("server", false, "serve instead of calling EmptyCall") + address := flag.String("address", "localhost:10000", "peer address") + caFile := flag.String("ca", "", "trusted peer CA PEM") + certFile := flag.String("cert", "", "identity certificate PEM") + keyFile := flag.String("key", "", "identity key PEM") + flag.Parse() + pem, err := os.ReadFile(*caFile) + if err != nil { + return err + } + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(pem) { + return fmt.Errorf("invalid CA PEM") + } + config := &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: roots} + if *certFile != "" { + identity, err := tls.LoadX509KeyPair(*certFile, *keyFile) + if err != nil { + return err + } + config.Certificates = []tls.Certificate{identity} + } + if *server { + config.ClientCAs = roots + config.ClientAuth = tls.RequireAndVerifyClientCert + listener, err := net.Listen("tcp", *address) + if err != nil { + return err + } + defer listener.Close() + s := grpc.NewServer(grpc.Creds(credentials.NewTLS(config))) + defer s.Stop() + pb.RegisterTestServiceServer(s, interop.NewTestServer()) + return s.Serve(listener) + } + conn, err := grpc.NewClient(*address, grpc.WithTransportCredentials(credentials.NewTLS(config))) + if err != nil { + return err + } + defer conn.Close() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, err = pb.NewTestServiceClient(conn).EmptyCall(ctx, &pb.Empty{}) + return err +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/tandem/vendor/grpc-lite/tests/official/run_tls.sh b/tandem/vendor/grpc-lite/tests/official/run_tls.sh index c86e02299..260d3ffd7 100755 --- a/tandem/vendor/grpc-lite/tests/official/run_tls.sh +++ b/tandem/vendor/grpc-lite/tests/official/run_tls.sh @@ -102,3 +102,43 @@ for test_case in "${cases[@]}"; do --use_tls=true \ --ca_file="$certificate" done +stop_peer + +# Mutual TLS: distinct server and client trust roots, both RPC directions. +mtls="$project_root/src/testdata/mtls" +(cd "$project_root/tests/official" && go build -mod=readonly -o "$work_dir/mtls-peer" ./mtls_peer) +peer_log="$work_dir/grpc-lite-mtls-server.log" +"$project_root/zig-out/bin/grpc-lite-interop-server" \ + --port="$zig_server_port" --use_tls=true \ + --tls_cert_file="$certificate" --tls_key_file="$private_key" \ + --client_ca_file="$mtls/ca.pem" >"$peer_log" 2>&1 & +peer_pid=$! +wait_for_peer "$zig_server_port" +run_case 'grpc-go client -> grpc-lite mTLS server' empty_unary \ + "$work_dir/mtls-peer" --address="localhost:$zig_server_port" \ + --ca="$certificate" --cert="$mtls/client.pem" --key="$mtls/client-key.pem" +for identity in missing untrusted expired; do + identity_args=() + case "$identity" in + untrusted) identity_args=(--cert="$certificate" --key="$private_key") ;; + expired) identity_args=(--cert="$mtls/expired-client.pem" --key="$mtls/client-key.pem") ;; + esac + if "$work_dir/mtls-peer" --address="localhost:$zig_server_port" --ca="$certificate" "${identity_args[@]}"; then + printf 'unexpected acceptance of %s client identity\n' "$identity" >&2 + exit 1 + fi + printf '[ PASS ] rejected %s client identity\n' "$identity" +done +stop_peer +peer_log="$work_dir/grpc-go-mtls-server.log" +"$work_dir/mtls-peer" --server --address="127.0.0.1:$go_server_port" \ + --ca="$mtls/ca.pem" --cert="$certificate" --key="$private_key" >"$peer_log" 2>&1 & +peer_pid=$! +wait_for_peer "$go_server_port" +for test_case in "${cases[@]}"; do + run_case 'grpc-lite mTLS client -> grpc-go server' "$test_case" \ + "$project_root/zig-out/bin/grpc-lite-interop-client" \ + --server_host=127.0.0.1 --server_port="$go_server_port" \ + --test_case="$test_case" --use_tls=true --ca_file="$certificate" \ + --tls_cert_file="$mtls/client.pem" --tls_key_file="$mtls/client-key.pem" +done diff --git a/tandem/vendor/grpc-lite/tools/build_native.sh b/tandem/vendor/grpc-lite/tools/build_native.sh index c857140cd..9582ee561 100755 --- a/tandem/vendor/grpc-lite/tools/build_native.sh +++ b/tandem/vendor/grpc-lite/tools/build_native.sh @@ -16,9 +16,7 @@ sanitize_c=${12} enable_gperftools=${13} enable_tcmalloc=${14} -c_flags=(-fvisibility=hidden -include limits.h) - -mkdir -p "$build_dir" +c_flags=(-fvisibility=hidden) if [[ "$sanitize_thread" == true && "$sanitize_c" == true ]]; then printf 'ThreadSanitizer and UndefinedBehaviorSanitizer are mutually exclusive\n' >&2 @@ -41,109 +39,37 @@ if [[ "$sanitize_thread" == true || "$sanitize_c" == true ]]; then c_flags+=(-g) fi -build_zig_static() { - local library=$1 - local include_dir=$2 - local source_glob=$3 - local archive="$build_dir/lib/lib${library}.a" - local object_dir="$build_dir/objects" - mkdir -p "$object_dir" "$(dirname "$archive")" - case "$library" in - nghttp2) - cat >"$build_dir/config.h" <<'EOF' -#define HAVE_ARPA_INET_H 1 -#define HAVE_NETINET_IN_H 1 -#define HAVE_NETDB_H 1 -#define HAVE_SYS_SOCKET_H 1 -#define HAVE_SYS_UIO_H 1 -#define HAVE_SYS_IOCTL_H 1 -#define HAVE_ERRNO_H 1 -#define HAVE_UNISTD_H 1 -#define HAVE_TIME_H 1 -#define HAVE_CLOCK_GETTIME_MONOTONIC 1 -#define HAVE_FCNTL_H 1 -#define HAVE_FCNTL_O_NONBLOCK 1 -#define HAVE_WRITEV 1 -#define RECVFROM_TYPE_ARG3 size_t -#define SEND_TYPE_ARG1 int -#define SEND_TYPE_ARG2 const void * -#define SEND_TYPE_ARG3 size_t -#define SEND_TYPE_ARG4 int -#define HAVE_PF_INET6 1 -#define HAVE_AF_INET6 1 -#define HAVE_STRUCT_SOCKADDR_IN6 1 -#define HAVE_STRUCT_ADDRINFO 1 -EOF - mkdir -p "$build_dir/include/nghttp2" - sed -e 's/@PACKAGE_VERSION@/1.61.0/g' \ - -e 's/@PACKAGE_VERSION_NUM@/0x013d00/g' \ - "$source_dir/lib/includes/nghttp2/nghttp2ver.h.in" \ - >"$build_dir/include/nghttp2/nghttp2ver.h" - ;; - cares) - cat >"$build_dir/ares_config.h" <<'EOF' -#define HAVE_ARPA_INET_H 1 -#define HAVE_NETINET_IN_H 1 -#define HAVE_NETDB_H 1 -#define HAVE_SYS_SOCKET_H 1 -#define HAVE_SYS_SELECT_H 1 -#define HAVE_SYS_TIME_H 1 -#define HAVE_SYS_UIO_H 1 -#define HAVE_SYS_IOCTL_H 1 -#define HAVE_ERRNO_H 1 -#define HAVE_UNISTD_H 1 -#define HAVE_TIME_H 1 -#define HAVE_CLOCK_GETTIME_MONOTONIC 1 -#define HAVE_FCNTL_H 1 -#define HAVE_FCNTL_O_NONBLOCK 1 -#define HAVE_WRITEV 1 -#define RECVFROM_TYPE_ARG3 size_t -#define SEND_TYPE_ARG1 int -#define SEND_TYPE_ARG2 const void * -#define SEND_TYPE_ARG3 size_t -#define SEND_TYPE_ARG4 int -#define HAVE_STRUCT_TIMEVAL 1 -#define HAVE_IPV6 1 -#define HAVE_PF_INET6 1 -#define HAVE_AF_INET6 1 -#define HAVE_STRUCT_SOCKADDR_IN6 1 -#define HAVE_STRUCT_ADDRINFO 1 -EOF - ;; - esac - - sources=() - while IFS= read -r source; do - sources+=("$source") - done < <(find "$source_dir/$source_glob" -type f -name '*.c' | sort) - if [[ ${#sources[@]} -eq 0 ]]; then - printf 'no C sources found for %s\n' "$library" >&2 - exit 1 - fi - - objects=() - for source in "${sources[@]}"; do - object="$object_dir/$(basename "${source%.c}").o" - "$zig_exe" cc -target "$target_triple" "${c_flags[@]}" \ - -I"$build_dir" -I"$build_dir/include" -I"$include_dir" -I"$source_dir" -I"$source_dir/src" -I"$source_dir/src/lib" -I"$source_dir/src/lib/include" \ - -D_GNU_SOURCE -DHAVE_CONFIG_H=0 -c "$source" -o "$object" - objects+=("$object") - done - "$zig_exe" ar rcs "$archive" "${objects[@]}" -} +common_options=( + -S "$source_dir" + -B "$build_dir" + -G Ninja + "-DCMAKE_BUILD_TYPE=$build_type" + "-DCMAKE_C_FLAGS=${c_flags[*]}" + "-DCMAKE_CXX_FLAGS=${c_flags[*]}" + -DBUILD_SHARED_LIBS=OFF +) case "$library" in nghttp2) - build_zig_static nghttp2 "$source_dir/lib/includes" lib - mkdir -p "$build_dir/lib/includes" - cp -a "$source_dir/lib/includes/." "$build_dir/lib/includes/" - exit 0 + CC="$cc" CXX="$cxx" cmake "${common_options[@]}" \ + -DENABLE_LIB_ONLY=ON \ + -DENABLE_APP=OFF \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_HPACK_TOOLS=OFF \ + -DENABLE_DOC=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_STATIC_LIBS=ON ;; cares) - build_zig_static cares "$source_dir/include" src/lib - mkdir -p "$build_dir/include" - cp -a "$source_dir/include/." "$build_dir/include/" - exit 0 + CC="$cc" CXX="$cxx" cmake "${common_options[@]}" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DCARES_STATIC=ON \ + -DCARES_SHARED=OFF \ + -DCARES_INSTALL=OFF \ + -DCARES_BUILD_TESTS=OFF \ + -DCARES_BUILD_CONTAINER_TESTS=OFF \ + -DCARES_BUILD_TOOLS=OFF \ + -DCARES_THREADS=ON ;; mbedtls) source_copy="$build_dir/source" @@ -182,11 +108,54 @@ case "$library" in exit 0 ;; gperftools) - printf 'gperftools is not supported by the Zig-only native builder\n' >&2 - exit 1 + enable_heap_profiler=false + if [[ "$enable_gperftools" == true && "$enable_tcmalloc" == true ]]; then + enable_heap_profiler=true + fi + CC="$cc" CXX="$cxx" cmake "${common_options[@]}" \ + -DBUILD_TESTING=OFF \ + -DGPERFTOOLS_BUILD_CPU_PROFILER="$enable_gperftools" \ + -DGPERFTOOLS_BUILD_HEAP_PROFILER="$enable_heap_profiler" \ + -DGPERFTOOLS_BUILD_DEBUGALLOC=OFF \ + -Dgperftools_build_minimal=OFF \ + -Dgperftools_build_benchmark=OFF \ + -Dgperftools_enable_libunwind=OFF \ + -Dgperftools_enable_frame_pointers=ON + if [[ "$enable_heap_profiler" == true ]]; then + targets=(tcmalloc profiler) + archives=(libtcmalloc.a libprofiler.a libstacktrace.a liblow_level_alloc.a libcommon.a) + elif [[ "$enable_tcmalloc" == true ]]; then + targets=(tcmalloc_minimal) + archives=(libtcmalloc_minimal.a libcommon.a) + else + targets=(profiler) + archives=(libprofiler.a libstacktrace.a libcommon.a) + fi + cmake --build "$build_dir" --target "${targets[@]}" + archive="$build_dir/libgrpc_lite_gperftools.a" + rm -f "$archive" + { + printf 'CREATE %s\n' "$archive" + for dependency in "${archives[@]}"; do + printf 'ADDLIB %s/%s\n' "$build_dir" "$dependency" + done + printf 'SAVE\nEND\n' + } | "$zig_exe" ar -M + gperftools_define=0 + tcmalloc_define=0 + [[ "$enable_gperftools" == true ]] && gperftools_define=1 + [[ "$enable_tcmalloc" == true ]] && tcmalloc_define=1 + "$zig_exe" cc -target "$target_triple" "${c_flags[@]}" \ + -DGRPC_LITE_ENABLE_GPERFTOOLS="$gperftools_define" \ + -DGRPC_LITE_ENABLE_TCMALLOC="$tcmalloc_define" \ + -c "$force_link_source" \ + -o "$build_dir/gperftools_force_link.o" + exit 0 ;; *) printf 'unsupported native library: %s\n' "$library" >&2 exit 1 ;; esac + +cmake --build "$build_dir" diff --git a/tandem/vendor/grpc-lite/tools/mbedtls_user_config.h b/tandem/vendor/grpc-lite/tools/mbedtls_user_config.h index 2179487cd..d1af4878d 100644 --- a/tandem/vendor/grpc-lite/tools/mbedtls_user_config.h +++ b/tandem/vendor/grpc-lite/tools/mbedtls_user_config.h @@ -4,4 +4,8 @@ #define MBEDTLS_THREADING_C #define MBEDTLS_THREADING_PTHREAD +/* ECP self-test counters are global and unsynchronized, even in threaded builds. + * Runtime TLS does not need the standalone mbedTLS self-test entry points. */ +#undef MBEDTLS_SELF_TEST + #endif diff --git a/tandem/vendor/grpc-lite/tools/package_transpiled_c.sh b/tandem/vendor/grpc-lite/tools/package_transpiled_c.sh index d1b5256eb..eaaeb924e 100755 --- a/tandem/vendor/grpc-lite/tools/package_transpiled_c.sh +++ b/tandem/vendor/grpc-lite/tools/package_transpiled_c.sh @@ -11,6 +11,7 @@ if [[ $# -ne 1 ]]; then fi project_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$project_root/tools/transpiled_c_target.sh" dist_dir=$(mkdir -p "$1" && cd "$1" && pwd) work_dir=$(mktemp -d "${TMPDIR:-/tmp}/grpc-lite-package-transpiled-c.XXXXXX") cleanup() { @@ -64,7 +65,7 @@ if [[ "$actual_files" != "$expected_files" ]]; then exit 1 fi -python3 - "$work_dir/generated/manifest.json" "$zon_version" "$actual_zig" "$project_root" <<'PY' +python3 - "$work_dir/generated/manifest.json" "$zon_version" "$actual_zig" "$project_root" "$transpiled_c_target" <<'PY' import json import subprocess import sys @@ -78,7 +79,7 @@ expected = { "dirty": bool(subprocess.check_output( ["git", "status", "--porcelain"], cwd=sys.argv[4], text=True).strip()), "zig_version": sys.argv[3], - "target": "x86_64-linux-gnu", + "target": sys.argv[5], "optimization": "ReleaseSafe", "tls": False, } @@ -86,7 +87,7 @@ if value != expected: raise SystemExit(f"manifest mismatch: expected {expected!r}, got {value!r}") PY -root="grpc-lite-transpiled-c-$zon_version-linux-x86_64" +root="grpc-lite-transpiled-c-$zon_version-linux-$transpiled_c_arch" mkdir -p "$work_dir/stage/$root/include" "$work_dir/stage/$root/src" cp "$work_dir/generated/manifest.json" "$work_dir/stage/$root/manifest.json" cp "$work_dir/generated/include/zig.h" "$work_dir/stage/$root/include/zig.h" diff --git a/tandem/vendor/grpc-lite/tools/transpile_c.sh b/tandem/vendor/grpc-lite/tools/transpile_c.sh index cd3420ddb..2040a28b5 100755 --- a/tandem/vendor/grpc-lite/tools/transpile_c.sh +++ b/tandem/vendor/grpc-lite/tools/transpile_c.sh @@ -6,6 +6,7 @@ export TZ=UTC umask 022 project_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$project_root/tools/transpiled_c_target.sh" output_dir=${1:-"$project_root/transpiled"} if [[ $# -gt 1 ]]; then @@ -54,7 +55,7 @@ mkdir -p "$output_dir" "$cache_dir" "$global_cache_dir" ( cd "$project_root" taskset -c "$selected_cpu" zig build transpile-c -j1 \ - -Dtarget=x86_64-linux-gnu \ + -Dtarget="$transpiled_c_target" \ -Dtranspile-c=true \ -Dprotobuf=true \ -Doptimize=ReleaseSafe \ @@ -66,7 +67,10 @@ mkdir -p "$output_dir" "$cache_dir" "$global_cache_dir" ) # Zig's x86 clone assembly uses LLVM-style comments, which GNU as rejects. -perl -pi -e 's{// SYS_}{# SYS_}g' "$output_dir/src/protoc-gen-grpc_lite_cpp.c" +# AArch64 uses // comments natively; rewriting them would break its assembly. +if [[ "$transpiled_c_target" == x86_64-linux-gnu ]]; then + perl -pi -e 's{// SYS_}{# SYS_}g' "$output_dir/src/protoc-gen-grpc_lite_cpp.c" +fi # Zig 0.16 emits unstable numeric suffixes for these otherwise identical tuple # types. Canonicalize the C identifiers so clean transpilation is reproducible. @@ -85,7 +89,7 @@ cat >"$output_dir/manifest.json" <&2; exit 2 ;; + esac +fi +case "$transpiled_c_target" in + x86_64-linux-gnu) transpiled_c_arch=x86_64 ;; + aarch64-linux-gnu) transpiled_c_arch=arm64 ;; + *) printf 'Unsupported transpiled C target: %s\n' "$transpiled_c_target" >&2; exit 2 ;; +esac +export GRPC_LITE_TRANSPILED_C_TARGET="$transpiled_c_target" From 2dfd1aaf5f4131b10e20dd1c6cb4e19441857ee8 Mon Sep 17 00:00:00 2001 From: Xiao Deshi Date: Sun, 18 Oct 2026 21:12:46 +0800 Subject: [PATCH 3/3] fix(tandem): adapt TlsOptions to grpc-lite v0.5.0 API grpc-lite v0.5.0 removed the standalone require_client_certificate field and renamed ca_certificates_pem to client_ca_certificates_pem. mTLS is now driven by whether the client CA list is non-null, so require_client_cert=false simply passes null. --- tandem/src/main.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tandem/src/main.zig b/tandem/src/main.zig index 6baf023cf..23d1e0c14 100644 --- a/tandem/src/main.zig +++ b/tandem/src/main.zig @@ -210,8 +210,11 @@ fn startGrpcLite(allocator: std.mem.Allocator, pipeline: *PipelineServer, config .tls = if (cert != null and key != null) .{ .certificate_chain_pem = cert.?, .private_key_pem = key.?, - .ca_certificates_pem = ca, - .require_client_certificate = config.require_client_cert, + // v0.5.0 dropped the standalone require_client_certificate field: + // mTLS is now driven by whether client_ca_certificates_pem is + // non-null. Passing null disables client verification, which is + // exactly require_client_cert=false. + .client_ca_certificates_pem = if (config.require_client_cert) ca else null, } else null, };