Skip to content

Commit c981081

Browse files
authored
dynamic_modules: scaffolds ABI header and compatibility check (#35626)
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 <[email protected]>
1 parent 20e2788 commit c981081

16 files changed

+264
-26
lines changed

.clang-tidy

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ CheckOptions:
8383
|Returns(Default)?WorkerId$|
8484
|^sched_getaffinity$|
8585
|^shutdownThread_$|
86+
|^envoy_dynamic_module(.*)$|
8687
|TEST|
8788
|^use_count$)
8889
- key: readability-identifier-naming.ParameterCase

source/extensions/dynamic_modules/BUILD

+10-3
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ envoy_extension_package()
1111
envoy_cc_library(
1212
name = "dynamic_modules_lib",
1313
srcs = [
14+
"abi.h",
1415
"dynamic_modules.cc",
1516
],
16-
hdrs = [
17-
"dynamic_modules.h",
18-
],
17+
hdrs = ["dynamic_modules.h"],
1918
deps = [
19+
":abi_version_lib",
2020
"//envoy/common:exception_lib",
2121
],
2222
)
23+
24+
envoy_cc_library(
25+
name = "abi_version_lib",
26+
hdrs = [
27+
"abi_version.h",
28+
],
29+
)
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#pragma once
2+
3+
// NOLINT(namespace-envoy)
4+
5+
// This is a pure C header file that defines the ABI of the core of dynamic modules used by Envoy.
6+
//
7+
// This must not contain any dependencies besides standard library since it is not only used by
8+
// Envoy itself but also by dynamic module SDKs written in non-C++ languages.
9+
//
10+
// Currently, compatibility is only guaranteed by an exact version match between the Envoy
11+
// codebase and the dynamic module SDKs. In the future, after the ABI is stabilized, we will revisit
12+
// this restriction and hopefully provide a wider compatibility guarantee. Until then, Envoy
13+
// checks the hash of the ABI header files to ensure that the dynamic modules are built against the
14+
// same version of the ABI.
15+
16+
#ifdef __cplusplus
17+
#include <cstddef>
18+
19+
extern "C" {
20+
#else
21+
#include <stddef.h>
22+
#endif
23+
24+
// -----------------------------------------------------------------------------
25+
// ---------------------------------- Types ------------------------------------
26+
// -----------------------------------------------------------------------------
27+
//
28+
// Types used in the ABI. The name of a type must be prefixed with "envoy_dynamic_module_type_".
29+
30+
/**
31+
* envoy_dynamic_module_type_abi_version represents a null-terminated string that contains the ABI
32+
* version of the dynamic module. This is used to ensure that the dynamic module is built against
33+
* the compatible version of the ABI.
34+
*/
35+
typedef const char* envoy_dynamic_module_type_abi_version; // NOLINT(modernize-use-using)
36+
37+
// -----------------------------------------------------------------------------
38+
// ------------------------------- Event Hooks ---------------------------------
39+
// -----------------------------------------------------------------------------
40+
//
41+
// Event hooks are functions that are called by Envoy in response to certain events.
42+
// The module must implement and export these functions in the dynamic module object file.
43+
//
44+
// Each event hook is defined as a function prototype. The symbol must be prefixed with
45+
// "envoy_dynamic_module_on_".
46+
47+
/**
48+
* envoy_dynamic_module_on_program_init is called by the main thread exactly when the module is
49+
* loaded. The function returns the ABI version of the dynamic module. If null is returned, the
50+
* module will be unloaded immediately.
51+
*
52+
* For Envoy, the return value will be used to check the compatibility of the dynamic module.
53+
*
54+
* For dynamic modules, this is useful when they need to perform some process-wide
55+
* initialization or check if the module is compatible with the platform, such as CPU features.
56+
* Note that initialization routines of a dynamic module can also be performed without this function
57+
* through constructor functions in an object file. However, normal constructors cannot be used
58+
* to check compatibility and gracefully fail the initialization because there is no way to
59+
* report an error to Envoy.
60+
*
61+
* @return envoy_dynamic_module_type_abi_version is the ABI version of the dynamic module. Null
62+
* means the error and the module will be unloaded immediately.
63+
*/
64+
envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init();
65+
66+
#ifdef __cplusplus
67+
}
68+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#pragma once
2+
#ifdef __cplusplus
3+
namespace Envoy {
4+
namespace Extensions {
5+
namespace DynamicModules {
6+
#endif
7+
// This is the ABI version calculated as a sha256 hash of the ABI header files. When the ABI
8+
// changes, this value must change, and the correctness of this value is checked by the test.
9+
const char* kAbiVersion = "749b1e6bf97309b7d171009700a80e651ac61e35f9770c24a63460d765895a51";
10+
11+
#ifdef __cplusplus
12+
} // namespace DynamicModules
13+
} // namespace Extensions
14+
} // namespace Envoy
15+
#endif

source/extensions/dynamic_modules/dynamic_modules.cc

+26-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
#include "envoy/common/exception.h"
99

10+
#include "source/extensions/dynamic_modules/abi.h"
11+
#include "source/extensions/dynamic_modules/abi_version.h"
12+
1013
namespace Envoy {
1114
namespace Extensions {
1215
namespace DynamicModules {
@@ -27,7 +30,29 @@ absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view
2730
return absl::InvalidArgumentError(
2831
absl::StrCat("Failed to load dynamic module: ", object_file_path, " : ", dlerror()));
2932
}
30-
return std::make_shared<DynamicModule>(handle);
33+
34+
DynamicModuleSharedPtr dynamic_module = std::make_shared<DynamicModule>(handle);
35+
36+
const auto init_function =
37+
dynamic_module->getFunctionPointer<decltype(&envoy_dynamic_module_on_program_init)>(
38+
"envoy_dynamic_module_on_program_init");
39+
40+
if (init_function == nullptr) {
41+
return absl::InvalidArgumentError(
42+
absl::StrCat("Failed to resolve envoy_dynamic_module_on_program_init: ", dlerror()));
43+
}
44+
45+
const char* abi_version = (*init_function)();
46+
if (abi_version == nullptr) {
47+
return absl::InvalidArgumentError(
48+
absl::StrCat("Failed to initialize dynamic module: ", object_file_path));
49+
}
50+
// Checks the kAbiVersion and the version of the dynamic module.
51+
if (absl::string_view(abi_version) != absl::string_view(kAbiVersion)) {
52+
return absl::InvalidArgumentError(
53+
absl::StrCat("ABI version mismatch: got ", abi_version, ", but expected ", kAbiVersion));
54+
}
55+
return dynamic_module;
3156
}
3257

3358
DynamicModule::~DynamicModule() { dlclose(handle_); }

test/extensions/dynamic_modules/BUILD

+19-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,29 @@ envoy_cc_test(
1212
name = "dynamic_modules_test",
1313
srcs = ["dynamic_modules_test.cc"],
1414
data = [
15-
"//test/extensions/dynamic_modules/test_data:no_op",
15+
"//test/extensions/dynamic_modules/test_data/c:abi_version_mismatch",
16+
"//test/extensions/dynamic_modules/test_data/c:no_op",
17+
"//test/extensions/dynamic_modules/test_data/c:no_program_init",
18+
"//test/extensions/dynamic_modules/test_data/c:program_init_fail",
1619
],
1720
deps = [
1821
"//source/extensions/dynamic_modules:dynamic_modules_lib",
1922
"//test/test_common:environment_lib",
2023
"//test/test_common:utility_lib",
2124
],
2225
)
26+
27+
envoy_cc_test(
28+
name = "abi_version_test",
29+
srcs = ["abi_version_test.cc"],
30+
data = [
31+
"//source/extensions/dynamic_modules:abi.h",
32+
],
33+
deps = [
34+
"//source/common/common:hex_lib",
35+
"//source/common/crypto:utility_lib",
36+
"//source/extensions/dynamic_modules:abi_version_lib",
37+
"//test/test_common:environment_lib",
38+
"//test/test_common:utility_lib",
39+
],
40+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#include <memory>
2+
3+
#include "envoy/common/exception.h"
4+
5+
#include "source/common/common/hex.h"
6+
#include "source/common/crypto/utility.h"
7+
#include "source/extensions/dynamic_modules/abi_version.h"
8+
9+
#include "test/test_common/environment.h"
10+
#include "test/test_common/utility.h"
11+
12+
#include "gtest/gtest.h"
13+
14+
namespace Envoy {
15+
namespace Extensions {
16+
namespace DynamicModules {
17+
18+
// This test ensure that abi_version.h contains the correct sha256 hash of ABI header files.
19+
TEST(DynamicModules, ABIVersionCheck) {
20+
const auto abi_header_path =
21+
TestEnvironment::substitute("{{ test_rundir }}/source/extensions/dynamic_modules/abi.h");
22+
// Read the header file and calculate the sha256 hash.
23+
const std::string abi_header = TestEnvironment::readFileToStringForTest(abi_header_path);
24+
const std::string sha256 =
25+
Hex::encode(Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(
26+
Buffer::OwnedImpl(abi_header)));
27+
EXPECT_EQ(sha256, kAbiVersion);
28+
}
29+
30+
} // namespace DynamicModules
31+
} // namespace Extensions
32+
} // namespace Envoy

test/extensions/dynamic_modules/dynamic_modules_test.cc

+60-8
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,37 @@ namespace Extensions {
1414
namespace DynamicModules {
1515

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

23-
TEST(DynamicModuleTest, InvalidPath) {
23+
TEST(DynamicModuleTestGeneral, InvalidPath) {
2424
absl::StatusOr<DynamicModuleSharedPtr> result = newDynamicModule("invalid_name", false);
2525
EXPECT_FALSE(result.ok());
2626
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
2727
}
2828

29-
TEST(DynamicModuleTest, LoadNoOp) {
29+
/**
30+
* Class to test the identical behavior of the dynamic module in different languages.
31+
*/
32+
class DynamicModuleTestLanguages : public ::testing::TestWithParam<std::string> {
33+
public:
34+
static std::string languageParamToTestName(const ::testing::TestParamInfo<std::string>& info) {
35+
return info.param;
36+
};
37+
};
38+
39+
INSTANTIATE_TEST_SUITE_P(LanguageTests, DynamicModuleTestLanguages,
40+
testing::Values("c"), // TODO: Other languages.
41+
DynamicModuleTestLanguages::languageParamToTestName);
42+
43+
TEST_P(DynamicModuleTestLanguages, DoNotClose) {
44+
std::string language = GetParam();
3045
using GetSomeVariableFuncType = int (*)();
3146
absl::StatusOr<DynamicModuleSharedPtr> module =
32-
newDynamicModule(testSharedObjectPath("no_op"), false);
47+
newDynamicModule(testSharedObjectPath("no_op", language), false);
3348
EXPECT_TRUE(module.ok());
3449
const auto getSomeVariable =
3550
module->get()->getFunctionPointer<GetSomeVariableFuncType>("getSomeVariable");
@@ -39,8 +54,8 @@ TEST(DynamicModuleTest, LoadNoOp) {
3954

4055
// Release the module, and reload it.
4156
module->reset();
42-
module =
43-
newDynamicModule(testSharedObjectPath("no_op"), true); // This time, do not close the module.
57+
module = newDynamicModule(testSharedObjectPath("no_op", language),
58+
true); // This time, do not close the module.
4459
EXPECT_TRUE(module.ok());
4560

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

5469
// Release the module, and reload it.
5570
module->reset();
56-
module = newDynamicModule(testSharedObjectPath("no_op"), false);
71+
module = newDynamicModule(testSharedObjectPath("no_op", language), false);
5772
EXPECT_TRUE(module.ok());
5873

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

81+
TEST_P(DynamicModuleTestLanguages, LoadNoOp) {
82+
std::string language = GetParam();
83+
absl::StatusOr<DynamicModuleSharedPtr> module =
84+
newDynamicModule(testSharedObjectPath("no_op", language), false);
85+
EXPECT_TRUE(module.ok());
86+
}
87+
88+
TEST_P(DynamicModuleTestLanguages, NoProgramInit) {
89+
std::string language = GetParam();
90+
absl::StatusOr<DynamicModuleSharedPtr> result =
91+
newDynamicModule(testSharedObjectPath("no_program_init", language), false);
92+
EXPECT_FALSE(result.ok());
93+
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
94+
EXPECT_THAT(result.status().message(),
95+
testing::HasSubstr("undefined symbol: envoy_dynamic_module_on_program_init"));
96+
}
97+
98+
TEST_P(DynamicModuleTestLanguages, ProgramInitFail) {
99+
std::string language = GetParam();
100+
absl::StatusOr<DynamicModuleSharedPtr> result =
101+
newDynamicModule(testSharedObjectPath("program_init_fail", language), false);
102+
EXPECT_FALSE(result.ok());
103+
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
104+
EXPECT_THAT(result.status().message(),
105+
testing::HasSubstr("Failed to initialize dynamic module:"));
106+
}
107+
108+
TEST_P(DynamicModuleTestLanguages, ABIVersionMismatch) {
109+
std::string language = GetParam();
110+
absl::StatusOr<DynamicModuleSharedPtr> result =
111+
newDynamicModule(testSharedObjectPath("abi_version_mismatch", language), false);
112+
EXPECT_FALSE(result.ok());
113+
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
114+
EXPECT_THAT(result.status().message(),
115+
testing::HasSubstr("ABI version mismatch: got invalid-version-hash, but expected"));
116+
}
117+
66118
} // namespace DynamicModules
67119
} // namespace Extensions
68120
} // namespace Envoy

test/extensions/dynamic_modules/test_data/BUILD

-7
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("//test/extensions/dynamic_modules/test_data/c:test_data.bzl", "test_program")
2+
3+
licenses(["notice"]) # Apache 2
4+
5+
package(default_visibility = ["//test/extensions/dynamic_modules:__pkg__"])
6+
7+
test_program(name = "no_op")
8+
9+
test_program(name = "no_program_init")
10+
11+
test_program(name = "program_init_fail")
12+
13+
test_program(name = "abi_version_mismatch")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#include "source/extensions/dynamic_modules/abi.h"
2+
3+
envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init() {
4+
return "invalid-version-hash";
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include "source/extensions/dynamic_modules/abi.h"
2+
#include "source/extensions/dynamic_modules/abi_version.h"
3+
4+
int getSomeVariable() {
5+
static int some_variable = 0;
6+
some_variable++;
7+
return some_variable;
8+
}
9+
10+
envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init() { return kAbiVersion; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
int foo() { return 0; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#include "source/extensions/dynamic_modules/abi.h"
2+
3+
envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init() { return NULL; }

test/extensions/dynamic_modules/test_data/test_data.bzl test/extensions/dynamic_modules/test_data/c/test_data.bzl

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ load("@rules_cc//cc:defs.bzl", "cc_library")
55
def test_program(name):
66
cc_library(
77
name = name,
8-
srcs = [name + ".c"],
8+
srcs = [name + ".c", "//source/extensions/dynamic_modules:abi.h", "//source/extensions/dynamic_modules:abi_version.h"],
99
linkopts = [
1010
"-shared",
1111
"-fPIC",

test/extensions/dynamic_modules/test_data/no_op.c

-5
This file was deleted.

0 commit comments

Comments
 (0)