SERVER-108453: Clean up malformed extensions and add two end-to-end startup failure test cases (#40123)

GitOrigin-RevId: d83d540d106759d6f1e6525764513d7c73daaacd
This commit is contained in:
Will Buerger 2025-08-15 15:36:14 -04:00 committed by MongoDB Bot
parent 0079e4c653
commit 1bd40dc8ad
20 changed files with 236 additions and 148 deletions

View File

@ -430,6 +430,10 @@ mongo_install(
extensions_with_config(
name = "dist_test_extensions",
srcs = [
# Any extension that we intend to load in extensions passthrough tests and in the
# extension-enabled variant MUST have the "_mongo_extension" suffix for the
# find_extensions.sh script to recognize it.
# TODO SERVER-108821: Remove the comment below once extension stages are no longer
# listed by $listMqlEntities.
# Any extension added here will result in the with-extensions Evergreen variant
@ -440,6 +444,11 @@ extensions_with_config(
# TODO SERVER-109108: Remove this entry when the bar extension is no longer needed.
"//src/mongo/db/extension/test_examples:bar_mongo_extension",
"//src/mongo/db/extension/test_examples:foo_mongo_extension",
# Any extension that is just loaded in a no-passthrough test MUST NOT have the
# "_mongo_extension" suffix.
"//src/mongo/db/extension/test_examples:no_symbol_bad_extension",
"//src/mongo/db/extension/test_examples:duplicate_stage_descriptor_bad_extension",
],
)

View File

@ -1494,3 +1494,87 @@ def mongo_cc_fuzzer_test(
exec_properties = exec_properties,
**kwargs
)
# Note: For these extensions to load successfully in the server, they must be built with
# --allocator=system. Otherwise, the extensions will get a local instance of tcmalloc which
# fails to run properly because there isn't enough TLS space available for both the host and
# extension's tcmalloc. In transitions.bzl, we define a Bazel transition for managing the allocator
# and other extension-specific options.
def mongo_cc_extension_shared_library(
name,
srcs = [],
deps = [],
header_deps = [],
visibility = None,
data = [],
tags = [],
copts = [],
linkopts = [],
includes = [],
linkstatic = False,
local_defines = [],
target_compatible_with = [],
defines = [],
additional_linker_inputs = [],
features = [],
exec_properties = {},
**kwargs):
mongo_cc_library(
name = name,
srcs = srcs,
deps = deps + [
"//src/mongo/db/extension/public:api",
"//src/mongo/db/extension/sdk:sdk_cpp",
],
header_deps = header_deps,
visibility = visibility,
data = data,
tags = tags,
copts = copts,
linkopts = linkopts,
includes = includes,
linkstatic = linkstatic,
local_defines = local_defines,
defines = defines,
features = features,
exec_properties = exec_properties,
additional_linker_inputs = additional_linker_inputs + select({
"@platforms//os:linux": [
":test_extensions.version_script.lds",
],
"//conditions:default": [],
}) + select({
"@platforms//os:macos": [
":test_extensions.exported_symbols_list.lds",
],
"//conditions:default": [],
}),
# linkshared produces a shared library as the output.
# TODO SERVER-109255 Make sure the test extensions are statically linked, as we expect
# all extensions to be.
linkshared = True,
non_transitive_dyn_linkopts = select({
"@platforms//os:linux": [
"-Wl,--version-script=$(location :test_extensions.version_script.lds)",
],
"//conditions:default": [],
}) + select({
"@platforms//os:macos": [
"-Wl,-exported_symbols_list,$(location :test_extensions.exported_symbols_list.lds)",
],
"//conditions:default": [],
}),
skip_global_deps = [
# This is a globally injected dependency. We don't want a special allocator linked
# here. Instead, the allocator should be overriden at load time.
"allocator",
"libunwind",
],
target_compatible_with = target_compatible_with + select({
"//bazel/config:shared_archive_or_link_dynamic": [],
"//conditions:default": ["@platforms//:incompatible"],
}) + select({
"@platforms//os:linux": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
)

View File

@ -1,13 +1,23 @@
/**
* Tests error cases when using the --loadExtensions startup parameter on mongos and mongod.
*
* @tags: [featureFlagExtensionsAPI]
* This includes testing cases where the host rejects the parsed options, file does not exist, and
* two cases where the extension is rejected by the host during loading.
*
* @tags: [
* featureFlagExtensionsAPI,
* # TODO SERVER-109351 Re-enable aubsan coverage by resolving memory leak
* incompatible_aubsan,
* ]
*/
import {isLinux} from "jstests/libs/os_helpers.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
const pathToExtensionFoo = MongoRunner.getExtensionPath("libfoo_mongo_extension.so");
const pathToMissingSymbolExtension = MongoRunner.getExtensionPath("libno_symbol_bad_extension.so");
const pathToDuplicateStageExtension =
MongoRunner.getExtensionPath("libduplicate_stage_descriptor_bad_extension.so");
// Create a ShardingTest so that we have a config DB for mongos to point to in our test. We don't
// use ShardingTest directly because repeated failed ShardingTest startups causes issues in the test
// environment. This also reduces the amount of times we have to start a whole sharded cluster in
@ -52,6 +62,10 @@ if (isLinux()) {
runTest({options: {loadExtensions: 12345}});
// Path to extension does not exist.
runTest({options: {loadExtensions: "path/does/not/exist.so"}});
// Path to extension with an .so that is missing the get_mongodb_extension symbol.
runTest({options: {loadExtensions: pathToMissingSymbolExtension}});
// Path to extension that attempts to register duplicate stage descriptors.
runTest({options: {loadExtensions: pathToDuplicateStageExtension}});
} else {
// Startup should fail because we are attempting to load an extension on a platform that is not
// linux.

View File

@ -59,19 +59,19 @@ mongo_cc_unit_test(
# TODO SERVER-109108: Remove this entry when the buzz extension is no longer needed.
"//src/mongo/db/extension/test_examples:buzz_mongo_extension",
"//src/mongo/db/extension/test_examples:foo_mongo_extension",
"//src/mongo/db/extension/test_examples:hostVersionFails_mongo_extension",
"//src/mongo/db/extension/test_examples:host_version_fails_bad_extension",
"//src/mongo/db/extension/test_examples:hostVersionSucceeds_mongo_extension",
"//src/mongo/db/extension/test_examples:initializeVersionFails_mongo_extension",
"//src/mongo/db/extension/test_examples:initialize_version_fails_bad_extension",
"//src/mongo/db/extension/test_examples:loadTwoStages_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed1_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed2_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed3_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed4_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed5_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed6_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed7_mongo_extension",
"//src/mongo/db/extension/test_examples:malformed8_mongo_extension",
"//src/mongo/db/extension/test_examples:nullStageDescriptor_mongo_extension",
"//src/mongo/db/extension/test_examples:no_symbol_bad_extension",
"//src/mongo/db/extension/test_examples:null_mongo_extension_bad_extension",
"//src/mongo/db/extension/test_examples:major_version_too_high_bad_extension",
"//src/mongo/db/extension/test_examples:major_version_too_low_bad_extension",
"//src/mongo/db/extension/test_examples:minor_version_too_high_bad_extension",
"//src/mongo/db/extension/test_examples:null_initialize_function_bad_extension",
"//src/mongo/db/extension/test_examples:major_version_max_int_bad_extension",
"//src/mongo/db/extension/test_examples:duplicate_stage_descriptor_bad_extension",
"//src/mongo/db/extension/test_examples:null_stage_descriptor_bad_extension",
],
tags = ["mongo_unittest_seventh_group"],
target_compatible_with = select({

View File

@ -50,40 +50,6 @@
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kDefault
namespace mongo::extension::host {
stdx::unordered_map<std::string, std::unique_ptr<SharedLibrary>> ExtensionLoader::loadedExtensions;
bool loadExtensions(const std::vector<std::string>& extensionPaths) {
if (extensionPaths.empty()) {
return true;
}
if (!feature_flags::gFeatureFlagExtensionsAPI.isEnabled()) {
LOGV2_ERROR(10668500,
"Extensions are not allowed with the current configuration. You may need to "
"enable featureFlagExtensionsAPI.");
return false;
}
for (const auto& extension : extensionPaths) {
LOGV2(10668501, "Loading extension", "filePath"_attr = extension);
try {
ExtensionLoader::load(extension);
} catch (...) {
LOGV2_ERROR(10668502,
"Error loading extension",
"filePath"_attr = extension,
"status"_attr = exceptionToStatus());
return false;
}
LOGV2(10668503, "Successfully loaded extension", "filePath"_attr = extension);
}
return true;
}
namespace {
void assertVersionCompatibility(const ::MongoExtensionAPIVersionVector* hostVersions,
const ::MongoExtensionAPIVersion& extensionVersion) {
@ -145,10 +111,43 @@ ExtensionHandle getMongoExtension(SharedLibrary& extensionLib, const std::string
}
} // namespace
stdx::unordered_map<std::string, std::unique_ptr<SharedLibrary>> ExtensionLoader::loadedExtensions;
bool loadExtensions(const std::vector<std::string>& extensionPaths) {
if (extensionPaths.empty()) {
return true;
}
if (!feature_flags::gFeatureFlagExtensionsAPI.isEnabled()) {
LOGV2_ERROR(10668500,
"Extensions are not allowed with the current configuration. You may need to "
"enable featureFlagExtensionsAPI.");
return false;
}
for (const auto& extension : extensionPaths) {
LOGV2(10668501, "Loading extension", "filePath"_attr = extension);
try {
ExtensionLoader::load(extension);
} catch (...) {
LOGV2_ERROR(10668502,
"Error loading extension",
"filePath"_attr = extension,
"status"_attr = exceptionToStatus());
return false;
}
LOGV2(10668503, "Successfully loaded extension", "filePath"_attr = extension);
}
return true;
}
void ExtensionLoader::load(const std::string& extensionPath) {
uassert(10845400,
str::stream() << "Loading extension '" << extensionPath
<< "' failed: " << "Extension has already been loaded",
str::stream() << "Loading extension '" << extensionPath << "' failed: "
<< "Extension has already been loaded",
!loadedExtensions.contains(extensionPath));
StatusWith<std::unique_ptr<SharedLibrary>> swExtensionLib =

View File

@ -76,45 +76,53 @@ TEST_F(LoadExtensionsTest, LoadExtensionErrorCases) {
AssertionException,
10615500);
// malformed1_extension is missing the get_mongodb_extension symbol definition.
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed1_mongo_extension.so")),
// no_symbol_bad_extension is missing the get_mongodb_extension symbol definition.
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libno_symbol_bad_extension.so")),
AssertionException,
10615501);
// malformed2_extension returns null from get_mongodb_extension.
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed2_mongo_extension.so")),
AssertionException,
10615503);
// null_mongo_extension_bad_extension returns null from get_mongodb_extension.
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libnull_mongo_extension_bad_extension.so")),
AssertionException,
10615503);
// malformed3_extension has an incompatible major version (plus 1).
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed3_mongo_extension.so")),
AssertionException,
10615504);
// major_version_too_high_bad_extension has an incompatible major version (plus 1).
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libmajor_version_too_high_bad_extension.so")),
AssertionException,
10615504);
// malformed4_extension has an incompatible major version (minus 1).
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed4_mongo_extension.so")),
AssertionException,
10615504);
// major_version_too_low_bad_extension has an incompatible major version (minus 1).
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libmajor_version_too_low_bad_extension.so")),
AssertionException,
10615504);
// malformed5_extension has an incompatible minor version.
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed5_mongo_extension.so")),
AssertionException,
10615505);
// minor_version_too_high_bad_extension has an incompatible minor version.
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libminor_version_too_high_bad_extension.so")),
AssertionException,
10615505);
// malformed6_extension has a null initialization function.
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed6_mongo_extension.so")),
AssertionException,
10615506);
// null_initialize_function_bad_extension has a null initialization function.
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libnull_initialize_function_bad_extension.so")),
AssertionException,
10615506);
// malformed7_extension has the maximum uint32_t value as its major version.
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed7_mongo_extension.so")),
AssertionException,
10615504);
// major_version_max_int_bad_extension has the maximum uint32_t value as its major version.
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libmajor_version_max_int_bad_extension.so")),
AssertionException,
10615504);
// malformed8_extension attempts to register the same stage descriptor multiple times.
ASSERT_THROWS_CODE(ExtensionLoader::load(getExtensionPath("libmalformed8_mongo_extension.so")),
AssertionException,
10696402);
// duplicate_stage_descriptor_bad_extension attempts to register the same stage descriptor
// multiple times.
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libduplicate_stage_descriptor_bad_extension.so")),
AssertionException,
10696402);
}
// TODO SERVER-109108: Switch this to use the foo extension once we can reset state in between
@ -189,20 +197,20 @@ TEST_F(LoadExtensionsTest, LoadExtensionHostVersionParameterSucceeds) {
TEST_F(LoadExtensionsTest, LoadExtensionHostVersionParameterFails) {
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libhostVersionFails_mongo_extension.so")),
ExtensionLoader::load(getExtensionPath("libhost_version_fails_bad_extension.so")),
AssertionException,
10615503);
}
TEST_F(LoadExtensionsTest, LoadExtensionInitializeVersionFails) {
TEST_F(LoadExtensionsTest, LoadExtensioninitialize_version_fails) {
ASSERT_THROWS_CODE(
ExtensionLoader::load(getExtensionPath("libinitializeVersionFails_mongo_extension.so")),
ExtensionLoader::load(getExtensionPath("libinitialize_version_fails_bad_extension.so")),
AssertionException,
10726600);
}
DEATH_TEST_F(LoadExtensionsTest, LoadExtensionNullStageDescriptor, "10596400") {
ExtensionLoader::load(getExtensionPath("libnullStageDescriptor_mongo_extension.so"));
DEATH_TEST_F(LoadExtensionsTest, LoadExtensionnull_stage_descriptor, "10596400") {
ExtensionLoader::load(getExtensionPath("libnull_stage_descriptor_bad_extension.so"));
}
TEST(LoadExtensionTest, LoadExtensionTwoStagesSucceeds) {

View File

@ -1,77 +1,51 @@
load("//bazel:mongo_src_rules.bzl", "mongo_cc_library", "mongo_cc_unit_test")
load("//bazel:mongo_src_rules.bzl", "mongo_cc_extension_shared_library", "mongo_cc_library", "mongo_cc_unit_test")
package(default_visibility = ["//visibility:public"])
# Extensions under test_examples/
[
# Note: For these extensions to load successfully in the server, they must be built with
# --allocator=system. Otherwise, the extensions will get a local instance of tcmalloc which
# fails to run properly because there isn't enough TLS space available for both the host and
# extension's tcmalloc. When testing on evergreen, we should build the extensions with the
# system allocator, and load the extensions into a host that was built with tcmalloc.
mongo_cc_library(
mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = [extension_name + ".cpp"],
additional_linker_inputs = select({
"@platforms//os:linux": [
":test_extensions.version_script.lds",
],
"//conditions:default": [],
}) + select({
"@platforms//os:macos": [
":test_extensions.exported_symbols_list.lds",
],
"//conditions:default": [],
}),
# linkshared produces a shared library as the output.
# TODO SERVER-109255 Make sure the test extensions are statically linked, as we expect
# all extensions to be.
linkshared = True,
non_transitive_dyn_linkopts = select({
"@platforms//os:linux": [
"-Wl,--version-script=$(location :test_extensions.version_script.lds)",
],
"//conditions:default": [],
}) + select({
"@platforms//os:macos": [
"-Wl,-exported_symbols_list,$(location :test_extensions.exported_symbols_list.lds)",
],
"//conditions:default": [],
}),
skip_global_deps = [
# This is a globally injected dependency. We don't want a special allocator linked
# here. Instead, the allocator should be overriden at load time.
"allocator",
"libunwind",
],
target_compatible_with = select({
"//bazel/config:shared_archive_or_link_dynamic": [],
"//conditions:default": ["@platforms//:incompatible"],
}) + select({
"@platforms//os:linux": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
deps = [
"//src/mongo/db/extension/public:api",
"//src/mongo/db/extension/sdk:sdk_cpp",
],
)
for extension_name in [
"malformed1",
"malformed2",
"malformed3",
"malformed4",
"malformed5",
"malformed6",
"malformed7",
"malformed8",
"foo",
"bar",
# TODO SERVER-109108: Remove this entry when the buzz extension is no longer needed.
"buzz",
"hostVersionSucceeds",
"hostVersionFails",
"initializeVersionFails",
"loadTwoStages",
"nullStageDescriptor",
]
]
# Extensions under test_examples/loading/
[
mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = ["loading/" + extension_name + ".cpp"],
)
for extension_name in [
"hostVersionSucceeds",
"loadTwoStages",
]
]
# Extensions under test_examples/fail_to_load/
# Each of these should fail startup.
[
mongo_cc_extension_shared_library(
name = extension_name + "_bad_extension",
srcs = ["fail_to_load/" + extension_name + ".cpp"],
)
for extension_name in [
"no_symbol",
"null_mongo_extension",
"major_version_too_high",
"major_version_too_low",
"minor_version_too_high",
"null_initialize_function",
"major_version_max_int",
"duplicate_stage_descriptor",
"host_version_fails",
"initialize_version_fails",
"null_stage_descriptor",
]
]

View File

@ -36,4 +36,4 @@ public:
};
// No definition of get_mongodb_extension() here, which is intentional to simulate a malformed
// extension.
// extension missing the export of the get_mongodb_extension symbol.