From 36cf2635687d2ed2da506ac91a9007a61931fed9 Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Fri, 28 Aug 2026 10:37:45 +0200 Subject: [PATCH 1/5] build: add isolated v2 FlatBuffers protocol library --- udf-runner-cpp/v2/BUILD.bazel | 44 +++++++++++++++++++ udf-runner-cpp/v2/MODULE.bazel | 1 + udf-runner-cpp/v2/udf_protocol.cc | 12 +++++ udf-runner-cpp/v2/udf_protocol.hpp | 19 ++++++++ .../v2/udf_protocol_symbol_leak_test.cc | 40 +++++++++++++++++ udf-runner-cpp/v2/udf_protocol_test.cc | 20 +++++++++ 6 files changed, 136 insertions(+) create mode 100644 udf-runner-cpp/v2/udf_protocol.cc create mode 100644 udf-runner-cpp/v2/udf_protocol.hpp create mode 100644 udf-runner-cpp/v2/udf_protocol_symbol_leak_test.cc create mode 100644 udf-runner-cpp/v2/udf_protocol_test.cc diff --git a/udf-runner-cpp/v2/BUILD.bazel b/udf-runner-cpp/v2/BUILD.bazel index 809d455..f06ea09 100644 --- a/udf-runner-cpp/v2/BUILD.bazel +++ b/udf-runner-cpp/v2/BUILD.bazel @@ -4,6 +4,50 @@ load("@rules_cc//cc:cc_test.bzl", "cc_test") package(default_visibility = ["//visibility:public"]) +genrule( + name = "udf_protocol_generated", + srcs = ["udf_protocol.fbs"], + outs = ["udf_protocol_generated.h"], + tools = ["@flatbuffers//:flatc"], + cmd = "$(location @flatbuffers//:flatc) --cpp -o $(@D) $(location udf_protocol.fbs)", +) + +cc_library( + name = "udf_protocol", + srcs = ["udf_protocol.cc"], + hdrs = ["udf_protocol.hpp", ":udf_protocol_generated"], + copts = ["-std=c++20"], + deps = ["@flatbuffers//:runtime_cc"], +) + +cc_test( + name = "udf_protocol_test", + srcs = ["udf_protocol_test.cc"], + copts = ["-std=c++20"], + deps = [":udf_protocol"], +) + +cc_binary( + name = "udf_protocol_shared", + srcs = ["udf_protocol.cc"], + copts = [ + "-std=c++20", + "-fvisibility=hidden", + "-fvisibility-inlines-hidden", + ], + linkshared = 1, + deps = [":udf_protocol"], +) + +cc_test( + name = "udf_protocol_symbol_leak_test", + srcs = ["udf_protocol_symbol_leak_test.cc"], + copts = ["-std=c++20"], + data = [":udf_protocol_shared"], + args = ["$(location :udf_protocol_shared)"], + target_compatible_with = ["@platforms//os:linux"], +) + alias( name = "arrow_core", actual = "@v2_arrow//:arrow_core", diff --git a/udf-runner-cpp/v2/MODULE.bazel b/udf-runner-cpp/v2/MODULE.bazel index 60e7bad..1372496 100644 --- a/udf-runner-cpp/v2/MODULE.bazel +++ b/udf-runner-cpp/v2/MODULE.bazel @@ -5,6 +5,7 @@ module( bazel_dep(name = "rules_cc", version = "0.2.17") bazel_dep(name = "platforms", version = "1.0.0") +bazel_dep(name = "flatbuffers", version = "25.2.10") http_archive = use_repo_rule( "@bazel_tools//tools/build_defs/repo:http.bzl", diff --git a/udf-runner-cpp/v2/udf_protocol.cc b/udf-runner-cpp/v2/udf_protocol.cc new file mode 100644 index 0000000..0ed1120 --- /dev/null +++ b/udf-runner-cpp/v2/udf_protocol.cc @@ -0,0 +1,12 @@ +#include "udf_protocol.hpp" + +namespace exasol::udf::protocol { + +bool VerifyFrameBuffer(const void* data, std::size_t size) { + using IsolatedVerifier = + exasol::udf::v2::third_party::flatbuffers::Verifier; + IsolatedVerifier verifier(static_cast(data), size); + return verifier.VerifyBuffer(); +} + +} // namespace exasol::udf::protocol diff --git a/udf-runner-cpp/v2/udf_protocol.hpp b/udf-runner-cpp/v2/udf_protocol.hpp new file mode 100644 index 0000000..e03dfd4 --- /dev/null +++ b/udf-runner-cpp/v2/udf_protocol.hpp @@ -0,0 +1,19 @@ +#ifndef EXASOL_UDF_V2_UDF_PROTOCOL_HPP_ +#define EXASOL_UDF_V2_UDF_PROTOCOL_HPP_ + +#include + +// The generated API uses FlatBuffers runtime types. Rewrite the runtime +// namespace only while parsing the generated header, so the global +// ::flatbuffers namespace is never part of this library's API or ABI. +#define flatbuffers exasol::udf::v2::third_party::flatbuffers +#include "udf_protocol_generated.h" +#undef flatbuffers + +namespace exasol::udf::protocol { + +bool VerifyFrameBuffer(const void* data, std::size_t size); + +} // namespace exasol::udf::protocol + +#endif // EXASOL_UDF_V2_UDF_PROTOCOL_HPP_ diff --git a/udf-runner-cpp/v2/udf_protocol_symbol_leak_test.cc b/udf-runner-cpp/v2/udf_protocol_symbol_leak_test.cc new file mode 100644 index 0000000..b1d58e6 --- /dev/null +++ b/udf-runner-cpp/v2/udf_protocol_symbol_leak_test.cc @@ -0,0 +1,40 @@ +#include +#include +#include +#include + +namespace { + +[[noreturn]] void fail(const std::string& message) { + throw std::runtime_error(message); +} + +void verify_symbols(const std::string& library_path) { + const std::string command = "nm -D --defined-only -- '" + library_path + "'"; + FILE* pipe = popen(command.c_str(), "r"); + if (pipe == nullptr) { + fail("cannot inspect protocol library symbols"); + } + + char line[4096]; + while (std::fgets(line, sizeof(line), pipe) != nullptr) { + const std::string symbol(line); + const std::size_t name_start = symbol.find_last_of(' '); + if (name_start != std::string::npos && + symbol.compare(name_start + 1, 16, "_ZN11flatbuffers") == 0) { + pclose(pipe); + fail("protocol library exports a global flatbuffers symbol: " + symbol); + } + } + + if (pclose(pipe) != 0) { + fail("cannot complete protocol library symbol inspection"); + } +} + +} // namespace + +int main(int argc, char** argv) { + assert(argc == 2); + verify_symbols(argv[1]); +} diff --git a/udf-runner-cpp/v2/udf_protocol_test.cc b/udf-runner-cpp/v2/udf_protocol_test.cc new file mode 100644 index 0000000..1d48ee7 --- /dev/null +++ b/udf-runner-cpp/v2/udf_protocol_test.cc @@ -0,0 +1,20 @@ +#include "udf_protocol.hpp" + +#include +#include + +int main() { + exasol::udf::v2::third_party::flatbuffers::FlatBufferBuilder builder; + const auto call_name = builder.CreateString("example"); + const auto open_call = exasol::udf::protocol::CreateOpenCall(builder, call_name); + const auto message = exasol::udf::protocol::CreateStreamMessage( + builder, 0, 0, open_call); + const auto frame = exasol::udf::protocol::CreateFrame(builder, 7, message); + builder.Finish(frame); + + assert(exasol::udf::protocol::VerifyFrameBuffer( + builder.GetBufferPointer(), builder.GetSize())); + const auto* decoded = exasol::udf::protocol::GetFrame(builder.GetBufferPointer()); + assert(decoded->stream_id() == 7); + assert(decoded->message()->open_call()->call_name()->str() == "example"); +} From d4c18165aa9bbfbce21ec583303aec0e03fec78e Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Fri, 28 Aug 2026 10:44:56 +0200 Subject: [PATCH 2/5] test: verify static FlatBuffers symbols are isolated --- udf-runner-cpp/v2/BUILD.bazel | 9 ++++ .../udf_protocol_static_symbol_leak_test.cc | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 udf-runner-cpp/v2/udf_protocol_static_symbol_leak_test.cc diff --git a/udf-runner-cpp/v2/BUILD.bazel b/udf-runner-cpp/v2/BUILD.bazel index f06ea09..4511c4b 100644 --- a/udf-runner-cpp/v2/BUILD.bazel +++ b/udf-runner-cpp/v2/BUILD.bazel @@ -48,6 +48,15 @@ cc_test( target_compatible_with = ["@platforms//os:linux"], ) +cc_test( + name = "udf_protocol_static_symbol_leak_test", + srcs = ["udf_protocol_static_symbol_leak_test.cc"], + copts = ["-std=c++20"], + data = [":udf_protocol"], + args = ["$(locations :udf_protocol)"], + target_compatible_with = ["@platforms//os:linux"], +) + alias( name = "arrow_core", actual = "@v2_arrow//:arrow_core", diff --git a/udf-runner-cpp/v2/udf_protocol_static_symbol_leak_test.cc b/udf-runner-cpp/v2/udf_protocol_static_symbol_leak_test.cc new file mode 100644 index 0000000..cfbf64d --- /dev/null +++ b/udf-runner-cpp/v2/udf_protocol_static_symbol_leak_test.cc @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include + +namespace { + +[[noreturn]] void fail(const std::string& message) { + throw std::runtime_error(message); +} + +void verify_symbols(const std::string& archive_path) { + const std::string command = "nm -g --defined-only -- '" + archive_path + "'"; + FILE* pipe = popen(command.c_str(), "r"); + if (pipe == nullptr) { + fail("cannot inspect protocol archive symbols"); + } + + char line[4096]; + while (std::fgets(line, sizeof(line), pipe) != nullptr) { + const std::string symbol(line); + const std::size_t name_start = symbol.find_last_of(' '); + if (name_start != std::string::npos && + symbol.compare(name_start + 1, 16, "_ZN11flatbuffers") == 0) { + pclose(pipe); + fail("protocol archive exports a global flatbuffers symbol: " + symbol); + } + } + + if (pclose(pipe) != 0) { + fail("cannot complete protocol archive symbol inspection"); + } +} + +} // namespace + +int main(int argc, char** argv) { + assert(argc > 1); + bool found_archive = false; + for (int index = 1; index < argc; ++index) { + const std::string_view path(argv[index]); + if (path.ends_with(".a")) { + verify_symbols(std::string(path)); + found_archive = true; + } + } + assert(found_archive); +} From cb158ba80767abb41c5e2eb051514fa53cbe7425 Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Fri, 28 Aug 2026 10:57:36 +0200 Subject: [PATCH 3/5] fix: isolate FlatBuffers runtime headers --- udf-runner-cpp/v2/BUILD.bazel | 69 ++++++++++++++++++- .../v2/flatbuffers_header_order_reverse.cc | 10 +++ .../v2/flatbuffers_header_order_test.cc | 14 ++++ .../v2/private_flatbuffers_headers.sh | 14 ++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 udf-runner-cpp/v2/flatbuffers_header_order_reverse.cc create mode 100644 udf-runner-cpp/v2/flatbuffers_header_order_test.cc create mode 100644 udf-runner-cpp/v2/private_flatbuffers_headers.sh diff --git a/udf-runner-cpp/v2/BUILD.bazel b/udf-runner-cpp/v2/BUILD.bazel index 4511c4b..c7c09b9 100644 --- a/udf-runner-cpp/v2/BUILD.bazel +++ b/udf-runner-cpp/v2/BUILD.bazel @@ -4,12 +4,67 @@ load("@rules_cc//cc:cc_test.bzl", "cc_test") package(default_visibility = ["//visibility:public"]) +FLATBUFFERS_PUBLIC_HEADERS = [ + "allocator.h", + "array.h", + "base.h", + "buffer.h", + "buffer_ref.h", + "code_generator.h", + "code_generators.h", + "default_allocator.h", + "detached_buffer.h", + "file_manager.h", + "flatbuffer_builder.h", + "flatbuffers.h", + "flex_flat_util.h", + "flexbuffers.h", + "grpc.h", + "hash.h", + "idl.h", + "minireflect.h", + "reflection.h", + "reflection_generated.h", + "registry.h", + "stl_emulation.h", + "string.h", + "struct.h", + "table.h", + "util.h", + "vector.h", + "vector_downward.h", + "verifier.h", +] + +genrule( + name = "private_flatbuffers_headers", + srcs = ["@flatbuffers//:public_headers"], + outs = [ + "private_flatbuffers/exasol/udf/v2/third_party/flatbuffers/" + header + for header in FLATBUFFERS_PUBLIC_HEADERS + ], + tools = ["private_flatbuffers_headers.sh"], + cmd = "bash $(location private_flatbuffers_headers.sh) " + + "$(location private_flatbuffers/exasol/udf/v2/third_party/flatbuffers/allocator.h) " + + "$(SRCS)", +) + +cc_library( + name = "private_flatbuffers_runtime", + hdrs = [":private_flatbuffers_headers"], + includes = ["private_flatbuffers"], +) + genrule( name = "udf_protocol_generated", srcs = ["udf_protocol.fbs"], outs = ["udf_protocol_generated.h"], tools = ["@flatbuffers//:flatc"], - cmd = "$(location @flatbuffers//:flatc) --cpp -o $(@D) $(location udf_protocol.fbs)", + cmd = "$(location @flatbuffers//:flatc) --cpp -o $(@D) $(location udf_protocol.fbs) && " + + "sed -i " + + "-e 's#flatbuffers/#exasol/udf/v2/third_party/flatbuffers/#g' " + + "-e 's/FLATBUFFERS_/EXASOL_UDF_V2_FLATBUFFERS_/g' " + + "$(@D)/udf_protocol_generated.h", ) cc_library( @@ -17,7 +72,7 @@ cc_library( srcs = ["udf_protocol.cc"], hdrs = ["udf_protocol.hpp", ":udf_protocol_generated"], copts = ["-std=c++20"], - deps = ["@flatbuffers//:runtime_cc"], + deps = [":private_flatbuffers_runtime"], ) cc_test( @@ -57,6 +112,16 @@ cc_test( target_compatible_with = ["@platforms//os:linux"], ) +cc_test( + name = "flatbuffers_header_order_test", + srcs = [ + "flatbuffers_header_order_test.cc", + "flatbuffers_header_order_reverse.cc", + ], + copts = ["-std=c++20"], + deps = [":udf_protocol", "@flatbuffers//:runtime_cc"], +) + alias( name = "arrow_core", actual = "@v2_arrow//:arrow_core", diff --git a/udf-runner-cpp/v2/flatbuffers_header_order_reverse.cc b/udf-runner-cpp/v2/flatbuffers_header_order_reverse.cc new file mode 100644 index 0000000..84ee43b --- /dev/null +++ b/udf-runner-cpp/v2/flatbuffers_header_order_reverse.cc @@ -0,0 +1,10 @@ +#include "udf_protocol.hpp" + +#include + +void isolated_flatbuffers_first() { + exasol::udf::v2::third_party::flatbuffers::FlatBufferBuilder isolated_builder; + ::flatbuffers::FlatBufferBuilder ordinary_builder; + (void)ordinary_builder; + (void)isolated_builder; +} diff --git a/udf-runner-cpp/v2/flatbuffers_header_order_test.cc b/udf-runner-cpp/v2/flatbuffers_header_order_test.cc new file mode 100644 index 0000000..75b4974 --- /dev/null +++ b/udf-runner-cpp/v2/flatbuffers_header_order_test.cc @@ -0,0 +1,14 @@ +#include + +#include "udf_protocol.hpp" + +void ordinary_flatbuffers_first() { + ::flatbuffers::FlatBufferBuilder ordinary_builder; + exasol::udf::v2::third_party::flatbuffers::FlatBufferBuilder isolated_builder; + (void)ordinary_builder; + (void)isolated_builder; +} + +int main() { + ordinary_flatbuffers_first(); +} diff --git a/udf-runner-cpp/v2/private_flatbuffers_headers.sh b/udf-runner-cpp/v2/private_flatbuffers_headers.sh new file mode 100644 index 0000000..a1d0537 --- /dev/null +++ b/udf-runner-cpp/v2/private_flatbuffers_headers.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +output_dir="$(dirname "$1")" +shift +mkdir -p "$output_dir" + +for source in "$@"; do + header="$(basename "$source")" + sed \ + -e 's#"flatbuffers/#"exasol/udf/v2/third_party/flatbuffers/#g' \ + -e 's/FLATBUFFERS_/EXASOL_UDF_V2_FLATBUFFERS_/g' \ + "$source" > "$output_dir/$header" +done From f1a2246f5dc262737da6f0dcfe4afa14bd32a765 Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Fri, 28 Aug 2026 11:04:49 +0200 Subject: [PATCH 4/5] fix: rely on namespace rewrite for shared library isolation --- udf-runner-cpp/v2/BUILD.bazel | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/udf-runner-cpp/v2/BUILD.bazel b/udf-runner-cpp/v2/BUILD.bazel index c7c09b9..d19cf8f 100644 --- a/udf-runner-cpp/v2/BUILD.bazel +++ b/udf-runner-cpp/v2/BUILD.bazel @@ -85,11 +85,7 @@ cc_test( cc_binary( name = "udf_protocol_shared", srcs = ["udf_protocol.cc"], - copts = [ - "-std=c++20", - "-fvisibility=hidden", - "-fvisibility-inlines-hidden", - ], + copts = ["-std=c++20"], linkshared = 1, deps = [":udf_protocol"], ) From b70e6985bb8e0156e1c71f8d93e1dbc0682a1291 Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Fri, 28 Aug 2026 11:11:54 +0200 Subject: [PATCH 5/5] docs: explain FlatBuffers Bazel isolation steps --- udf-runner-cpp/v2/BUILD.bazel | 25 +++++++++++++++++++ .../v2/private_flatbuffers_headers.sh | 6 +++++ 2 files changed, 31 insertions(+) diff --git a/udf-runner-cpp/v2/BUILD.bazel b/udf-runner-cpp/v2/BUILD.bazel index d19cf8f..cd8d76b 100644 --- a/udf-runner-cpp/v2/BUILD.bazel +++ b/udf-runner-cpp/v2/BUILD.bazel @@ -4,6 +4,10 @@ load("@rules_cc//cc:cc_test.bzl", "cc_test") package(default_visibility = ["//visibility:public"]) +# Keep this list synchronized with @flatbuffers//:public_headers. The private +# runtime below needs a separate copy of every public header so its include +# paths, include guards, and configuration macros cannot collide with an +# ordinary FlatBuffers installation in a consumer translation unit. FLATBUFFERS_PUBLIC_HEADERS = [ "allocator.h", "array.h", @@ -39,11 +43,16 @@ FLATBUFFERS_PUBLIC_HEADERS = [ genrule( name = "private_flatbuffers_headers", srcs = ["@flatbuffers//:public_headers"], + # Bazel genrules require explicit output files. The output paths form a + # private include tree matching the rewritten includes produced below. outs = [ "private_flatbuffers/exasol/udf/v2/third_party/flatbuffers/" + header for header in FLATBUFFERS_PUBLIC_HEADERS ], tools = ["private_flatbuffers_headers.sh"], + # Pass one generated output to the script as an output-directory anchor; + # $(SRCS) expands to the external FlatBuffers header files. The helper + # copies and rewrites each header into the private include tree. cmd = "bash $(location private_flatbuffers_headers.sh) " + "$(location private_flatbuffers/exasol/udf/v2/third_party/flatbuffers/allocator.h) " + "$(SRCS)", @@ -52,6 +61,9 @@ genrule( cc_library( name = "private_flatbuffers_runtime", hdrs = [":private_flatbuffers_headers"], + # This include path makes rewritten includes such as + # exasol/udf/v2/third_party/flatbuffers/base.h resolvable without exposing + # the ordinary flatbuffers/... include tree through this target. includes = ["private_flatbuffers"], ) @@ -60,6 +72,9 @@ genrule( srcs = ["udf_protocol.fbs"], outs = ["udf_protocol_generated.h"], tools = ["@flatbuffers//:flatc"], + # flatc emits the standard FlatBuffers runtime include path and macro + # names. Rewrite both after generation so the protocol header uses the + # private runtime and can coexist with another FlatBuffers version. cmd = "$(location @flatbuffers//:flatc) --cpp -o $(@D) $(location udf_protocol.fbs) && " + "sed -i " + "-e 's#flatbuffers/#exasol/udf/v2/third_party/flatbuffers/#g' " + @@ -72,6 +87,8 @@ cc_library( srcs = ["udf_protocol.cc"], hdrs = ["udf_protocol.hpp", ":udf_protocol_generated"], copts = ["-std=c++20"], + # Do not depend on the ordinary runtime headers: the generated protocol + # header must resolve all runtime includes through the isolated copy. deps = [":private_flatbuffers_runtime"], ) @@ -94,8 +111,11 @@ cc_test( name = "udf_protocol_symbol_leak_test", srcs = ["udf_protocol_symbol_leak_test.cc"], copts = ["-std=c++20"], + # The test inspects the built shared object with nm. data makes the + # artifact available at runtime and args passes its runfiles path. data = [":udf_protocol_shared"], args = ["$(location :udf_protocol_shared)"], + # nm and the inspected ELF dynamic symbol table are Linux-specific. target_compatible_with = ["@platforms//os:linux"], ) @@ -103,8 +123,11 @@ cc_test( name = "udf_protocol_static_symbol_leak_test", srcs = ["udf_protocol_static_symbol_leak_test.cc"], copts = ["-std=c++20"], + # cc_library produces multiple artifacts, so the test receives all + # locations and selects the static .a archive for nm inspection. data = [":udf_protocol"], args = ["$(locations :udf_protocol)"], + # nm and the inspected ELF archive are Linux-specific. target_compatible_with = ["@platforms//os:linux"], ) @@ -115,6 +138,8 @@ cc_test( "flatbuffers_header_order_reverse.cc", ], copts = ["-std=c++20"], + # Compile both include orders against the ordinary runtime and the + # isolated runtime to protect against header-guard and macro collisions. deps = [":udf_protocol", "@flatbuffers//:runtime_cc"], ) diff --git a/udf-runner-cpp/v2/private_flatbuffers_headers.sh b/udf-runner-cpp/v2/private_flatbuffers_headers.sh index a1d0537..224b54d 100644 --- a/udf-runner-cpp/v2/private_flatbuffers_headers.sh +++ b/udf-runner-cpp/v2/private_flatbuffers_headers.sh @@ -1,12 +1,18 @@ #!/usr/bin/env bash set -euo pipefail +# The first argument is the directory containing the generated private +# headers. All remaining arguments are the public FlatBuffers headers supplied +# by Bazel from @flatbuffers//:public_headers. output_dir="$(dirname "$1")" shift mkdir -p "$output_dir" for source in "$@"; do header="$(basename "$source")" + # Use a private include prefix so ordinary flatbuffers/... headers can be + # included in the same translation unit. Prefix every FlatBuffers macro + # to isolate include guards and configuration macros as well. sed \ -e 's#"flatbuffers/#"exasol/udf/v2/third_party/flatbuffers/#g' \ -e 's/FLATBUFFERS_/EXASOL_UDF_V2_FLATBUFFERS_/g' \