diff --git a/udf-runner-cpp/v2/BUILD.bazel b/udf-runner-cpp/v2/BUILD.bazel index 809d455..cd8d76b 100644 --- a/udf-runner-cpp/v2/BUILD.bazel +++ b/udf-runner-cpp/v2/BUILD.bazel @@ -4,6 +4,145 @@ 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", + "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"], + # 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)", +) + +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"], +) + +genrule( + name = "udf_protocol_generated", + 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' " + + "-e 's/FLATBUFFERS_/EXASOL_UDF_V2_FLATBUFFERS_/g' " + + "$(@D)/udf_protocol_generated.h", +) + +cc_library( + name = "udf_protocol", + 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"], +) + +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"], + linkshared = 1, + deps = [":udf_protocol"], +) + +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"], +) + +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"], +) + +cc_test( + name = "flatbuffers_header_order_test", + srcs = [ + "flatbuffers_header_order_test.cc", + "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"], +) + 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/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..224b54d --- /dev/null +++ b/udf-runner-cpp/v2/private_flatbuffers_headers.sh @@ -0,0 +1,20 @@ +#!/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' \ + "$source" > "$output_dir/$header" +done 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_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); +} 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"); +}