Skip to content

Commit

Permalink
dynamic_modules: scaffolds ABI header and compatibility check (#35626)
Browse files Browse the repository at this point in the history
Commit Message: dynamic_modules: scaffolds ABI header and compatibility
check
Additional Description:

This commit scaffolds the ABI header and implements the version check.
sha256 hash will be calculated on the ABI headers, and the result value
is used to check the compatibility.

Risk Level: N/A
Testing: unit 
Docs Changes: N/A
Release Notes: N/A
Platform Specific Features:
[Optional Runtime guard:]
[Optional Fixes #Issue]
[Optional Fixes commit #PR or SHA]
[Optional Deprecated:]
[Optional [API
Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):]

---------

Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
  • Loading branch information
mathetake authored Aug 25, 2024
1 parent 20e2788 commit c981081
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 26 deletions.
1 change: 1 addition & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ CheckOptions:
|Returns(Default)?WorkerId$|
|^sched_getaffinity$|
|^shutdownThread_$|
|^envoy_dynamic_module(.*)$|
|TEST|
|^use_count$)
- key: readability-identifier-naming.ParameterCase
Expand Down
13 changes: 10 additions & 3 deletions source/extensions/dynamic_modules/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ envoy_extension_package()
envoy_cc_library(
name = "dynamic_modules_lib",
srcs = [
"abi.h",
"dynamic_modules.cc",
],
hdrs = [
"dynamic_modules.h",
],
hdrs = ["dynamic_modules.h"],
deps = [
":abi_version_lib",
"//envoy/common:exception_lib",
],
)

envoy_cc_library(
name = "abi_version_lib",
hdrs = [
"abi_version.h",
],
)
68 changes: 68 additions & 0 deletions source/extensions/dynamic_modules/abi.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#pragma once

// NOLINT(namespace-envoy)

// This is a pure C header file that defines the ABI of the core of dynamic modules used by Envoy.
//
// This must not contain any dependencies besides standard library since it is not only used by
// Envoy itself but also by dynamic module SDKs written in non-C++ languages.
//
// Currently, compatibility is only guaranteed by an exact version match between the Envoy
// codebase and the dynamic module SDKs. In the future, after the ABI is stabilized, we will revisit
// this restriction and hopefully provide a wider compatibility guarantee. Until then, Envoy
// checks the hash of the ABI header files to ensure that the dynamic modules are built against the
// same version of the ABI.

#ifdef __cplusplus
#include <cstddef>

extern "C" {
#else
#include <stddef.h>
#endif

// -----------------------------------------------------------------------------
// ---------------------------------- Types ------------------------------------
// -----------------------------------------------------------------------------
//
// Types used in the ABI. The name of a type must be prefixed with "envoy_dynamic_module_type_".

/**
* envoy_dynamic_module_type_abi_version represents a null-terminated string that contains the ABI
* version of the dynamic module. This is used to ensure that the dynamic module is built against
* the compatible version of the ABI.
*/
typedef const char* envoy_dynamic_module_type_abi_version; // NOLINT(modernize-use-using)

// -----------------------------------------------------------------------------
// ------------------------------- Event Hooks ---------------------------------
// -----------------------------------------------------------------------------
//
// Event hooks are functions that are called by Envoy in response to certain events.
// The module must implement and export these functions in the dynamic module object file.
//
// Each event hook is defined as a function prototype. The symbol must be prefixed with
// "envoy_dynamic_module_on_".

/**
* envoy_dynamic_module_on_program_init is called by the main thread exactly when the module is
* loaded. The function returns the ABI version of the dynamic module. If null is returned, the
* module will be unloaded immediately.
*
* For Envoy, the return value will be used to check the compatibility of the dynamic module.
*
* For dynamic modules, this is useful when they need to perform some process-wide
* initialization or check if the module is compatible with the platform, such as CPU features.
* Note that initialization routines of a dynamic module can also be performed without this function
* through constructor functions in an object file. However, normal constructors cannot be used
* to check compatibility and gracefully fail the initialization because there is no way to
* report an error to Envoy.
*
* @return envoy_dynamic_module_type_abi_version is the ABI version of the dynamic module. Null
* means the error and the module will be unloaded immediately.
*/
envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init();

#ifdef __cplusplus
}
#endif
15 changes: 15 additions & 0 deletions source/extensions/dynamic_modules/abi_version.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#pragma once
#ifdef __cplusplus
namespace Envoy {
namespace Extensions {
namespace DynamicModules {
#endif
// This is the ABI version calculated as a sha256 hash of the ABI header files. When the ABI
// changes, this value must change, and the correctness of this value is checked by the test.
const char* kAbiVersion = "749b1e6bf97309b7d171009700a80e651ac61e35f9770c24a63460d765895a51";

#ifdef __cplusplus
} // namespace DynamicModules
} // namespace Extensions
} // namespace Envoy
#endif
27 changes: 26 additions & 1 deletion source/extensions/dynamic_modules/dynamic_modules.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

#include "envoy/common/exception.h"

#include "source/extensions/dynamic_modules/abi.h"
#include "source/extensions/dynamic_modules/abi_version.h"

namespace Envoy {
namespace Extensions {
namespace DynamicModules {
Expand All @@ -27,7 +30,29 @@ absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view
return absl::InvalidArgumentError(
absl::StrCat("Failed to load dynamic module: ", object_file_path, " : ", dlerror()));
}
return std::make_shared<DynamicModule>(handle);

DynamicModuleSharedPtr dynamic_module = std::make_shared<DynamicModule>(handle);

const auto init_function =
dynamic_module->getFunctionPointer<decltype(&envoy_dynamic_module_on_program_init)>(
"envoy_dynamic_module_on_program_init");

if (init_function == nullptr) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to resolve envoy_dynamic_module_on_program_init: ", dlerror()));
}

const char* abi_version = (*init_function)();
if (abi_version == nullptr) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to initialize dynamic module: ", object_file_path));
}
// Checks the kAbiVersion and the version of the dynamic module.
if (absl::string_view(abi_version) != absl::string_view(kAbiVersion)) {
return absl::InvalidArgumentError(
absl::StrCat("ABI version mismatch: got ", abi_version, ", but expected ", kAbiVersion));
}
return dynamic_module;
}

DynamicModule::~DynamicModule() { dlclose(handle_); }
Expand Down
20 changes: 19 additions & 1 deletion test/extensions/dynamic_modules/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,29 @@ envoy_cc_test(
name = "dynamic_modules_test",
srcs = ["dynamic_modules_test.cc"],
data = [
"//test/extensions/dynamic_modules/test_data:no_op",
"//test/extensions/dynamic_modules/test_data/c:abi_version_mismatch",
"//test/extensions/dynamic_modules/test_data/c:no_op",
"//test/extensions/dynamic_modules/test_data/c:no_program_init",
"//test/extensions/dynamic_modules/test_data/c:program_init_fail",
],
deps = [
"//source/extensions/dynamic_modules:dynamic_modules_lib",
"//test/test_common:environment_lib",
"//test/test_common:utility_lib",
],
)

envoy_cc_test(
name = "abi_version_test",
srcs = ["abi_version_test.cc"],
data = [
"//source/extensions/dynamic_modules:abi.h",
],
deps = [
"//source/common/common:hex_lib",
"//source/common/crypto:utility_lib",
"//source/extensions/dynamic_modules:abi_version_lib",
"//test/test_common:environment_lib",
"//test/test_common:utility_lib",
],
)
32 changes: 32 additions & 0 deletions test/extensions/dynamic_modules/abi_version_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include <memory>

#include "envoy/common/exception.h"

#include "source/common/common/hex.h"
#include "source/common/crypto/utility.h"
#include "source/extensions/dynamic_modules/abi_version.h"

#include "test/test_common/environment.h"
#include "test/test_common/utility.h"

#include "gtest/gtest.h"

namespace Envoy {
namespace Extensions {
namespace DynamicModules {

// This test ensure that abi_version.h contains the correct sha256 hash of ABI header files.
TEST(DynamicModules, ABIVersionCheck) {
const auto abi_header_path =
TestEnvironment::substitute("{{ test_rundir }}/source/extensions/dynamic_modules/abi.h");
// Read the header file and calculate the sha256 hash.
const std::string abi_header = TestEnvironment::readFileToStringForTest(abi_header_path);
const std::string sha256 =
Hex::encode(Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(
Buffer::OwnedImpl(abi_header)));
EXPECT_EQ(sha256, kAbiVersion);
}

} // namespace DynamicModules
} // namespace Extensions
} // namespace Envoy
68 changes: 60 additions & 8 deletions test/extensions/dynamic_modules/dynamic_modules_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,37 @@ namespace Extensions {
namespace DynamicModules {

// This loads a shared object file from the test_data directory.
std::string testSharedObjectPath(std::string name) {
std::string testSharedObjectPath(std::string name, std::string language) {
return TestEnvironment::substitute(
"{{ test_rundir }}/test/extensions/dynamic_modules/test_data/") +
"lib" + name + ".so";
language + "/lib" + name + ".so";
}

TEST(DynamicModuleTest, InvalidPath) {
TEST(DynamicModuleTestGeneral, InvalidPath) {
absl::StatusOr<DynamicModuleSharedPtr> result = newDynamicModule("invalid_name", false);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
}

TEST(DynamicModuleTest, LoadNoOp) {
/**
* Class to test the identical behavior of the dynamic module in different languages.
*/
class DynamicModuleTestLanguages : public ::testing::TestWithParam<std::string> {
public:
static std::string languageParamToTestName(const ::testing::TestParamInfo<std::string>& info) {
return info.param;
};
};

INSTANTIATE_TEST_SUITE_P(LanguageTests, DynamicModuleTestLanguages,
testing::Values("c"), // TODO: Other languages.
DynamicModuleTestLanguages::languageParamToTestName);

TEST_P(DynamicModuleTestLanguages, DoNotClose) {
std::string language = GetParam();
using GetSomeVariableFuncType = int (*)();
absl::StatusOr<DynamicModuleSharedPtr> module =
newDynamicModule(testSharedObjectPath("no_op"), false);
newDynamicModule(testSharedObjectPath("no_op", language), false);
EXPECT_TRUE(module.ok());
const auto getSomeVariable =
module->get()->getFunctionPointer<GetSomeVariableFuncType>("getSomeVariable");
Expand All @@ -39,8 +54,8 @@ TEST(DynamicModuleTest, LoadNoOp) {

// Release the module, and reload it.
module->reset();
module =
newDynamicModule(testSharedObjectPath("no_op"), true); // This time, do not close the module.
module = newDynamicModule(testSharedObjectPath("no_op", language),
true); // This time, do not close the module.
EXPECT_TRUE(module.ok());

// This module must be reloaded and the variable must be reset.
Expand All @@ -53,7 +68,7 @@ TEST(DynamicModuleTest, LoadNoOp) {

// Release the module, and reload it.
module->reset();
module = newDynamicModule(testSharedObjectPath("no_op"), false);
module = newDynamicModule(testSharedObjectPath("no_op", language), false);
EXPECT_TRUE(module.ok());

// This module must be the already loaded one, and the variable must be kept.
Expand All @@ -63,6 +78,43 @@ TEST(DynamicModuleTest, LoadNoOp) {
EXPECT_EQ(getSomeVariable3(), 4); // Start from 4.
}

TEST_P(DynamicModuleTestLanguages, LoadNoOp) {
std::string language = GetParam();
absl::StatusOr<DynamicModuleSharedPtr> module =
newDynamicModule(testSharedObjectPath("no_op", language), false);
EXPECT_TRUE(module.ok());
}

TEST_P(DynamicModuleTestLanguages, NoProgramInit) {
std::string language = GetParam();
absl::StatusOr<DynamicModuleSharedPtr> result =
newDynamicModule(testSharedObjectPath("no_program_init", language), false);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(result.status().message(),
testing::HasSubstr("undefined symbol: envoy_dynamic_module_on_program_init"));
}

TEST_P(DynamicModuleTestLanguages, ProgramInitFail) {
std::string language = GetParam();
absl::StatusOr<DynamicModuleSharedPtr> result =
newDynamicModule(testSharedObjectPath("program_init_fail", language), false);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(result.status().message(),
testing::HasSubstr("Failed to initialize dynamic module:"));
}

TEST_P(DynamicModuleTestLanguages, ABIVersionMismatch) {
std::string language = GetParam();
absl::StatusOr<DynamicModuleSharedPtr> result =
newDynamicModule(testSharedObjectPath("abi_version_mismatch", language), false);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(result.status().message(),
testing::HasSubstr("ABI version mismatch: got invalid-version-hash, but expected"));
}

} // namespace DynamicModules
} // namespace Extensions
} // namespace Envoy
7 changes: 0 additions & 7 deletions test/extensions/dynamic_modules/test_data/BUILD

This file was deleted.

13 changes: 13 additions & 0 deletions test/extensions/dynamic_modules/test_data/c/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
load("//test/extensions/dynamic_modules/test_data/c:test_data.bzl", "test_program")

licenses(["notice"]) # Apache 2

package(default_visibility = ["//test/extensions/dynamic_modules:__pkg__"])

test_program(name = "no_op")

test_program(name = "no_program_init")

test_program(name = "program_init_fail")

test_program(name = "abi_version_mismatch")
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "source/extensions/dynamic_modules/abi.h"

envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init() {
return "invalid-version-hash";
}
10 changes: 10 additions & 0 deletions test/extensions/dynamic_modules/test_data/c/no_op.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include "source/extensions/dynamic_modules/abi.h"
#include "source/extensions/dynamic_modules/abi_version.h"

int getSomeVariable() {
static int some_variable = 0;
some_variable++;
return some_variable;
}

envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init() { return kAbiVersion; }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
int foo() { return 0; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include "source/extensions/dynamic_modules/abi.h"

envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init() { return NULL; }
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ load("@rules_cc//cc:defs.bzl", "cc_library")
def test_program(name):
cc_library(
name = name,
srcs = [name + ".c"],
srcs = [name + ".c", "//source/extensions/dynamic_modules:abi.h", "//source/extensions/dynamic_modules:abi_version.h"],
linkopts = [
"-shared",
"-fPIC",
Expand Down
5 changes: 0 additions & 5 deletions test/extensions/dynamic_modules/test_data/no_op.c

This file was deleted.

0 comments on commit c981081

Please sign in to comment.