1
#include "source/extensions/dynamic_modules/dynamic_modules.h"
2

            
3
#include <dlfcn.h>
4
#include <unistd.h>
5

            
6
#include <cerrno>
7
#include <string>
8

            
9
#include "envoy/common/exception.h"
10

            
11
#include "source/common/common/utility.h"
12
#include "source/extensions/dynamic_modules/abi/abi.h"
13

            
14
#include "absl/strings/str_cat.h"
15

            
16
namespace Envoy {
17
namespace Extensions {
18
namespace DynamicModules {
19

            
20
constexpr char DYNAMIC_MODULES_SEARCH_PATH[] = "ENVOY_DYNAMIC_MODULES_SEARCH_PATH";
21

            
22
absl::StatusOr<DynamicModulePtr>
23
newDynamicModule(const std::filesystem::path& object_file_absolute_path, const bool do_not_close,
24
1044
                 const bool load_globally) {
25
  // From the man page of dlopen(3):
26
  //
27
  // > This can be used to test if the object is already resident (dlopen() returns NULL if it
28
  // > is not, or the object's handle if it is resident).
29
  //
30
  // So we can use RTLD_NOLOAD to check if the module is already loaded to avoid the duplicate call
31
  // to the init function.
32
1044
  void* handle = dlopen(object_file_absolute_path.c_str(), RTLD_NOLOAD | RTLD_LAZY);
33
1044
  if (handle != nullptr) {
34
    // This means the module is already loaded, and the return value is the handle of the already
35
    // loaded module. We don't need to call the init function again.
36
77
    return std::make_unique<DynamicModule>(handle);
37
77
  }
38
  // RTLD_LAZY is required for not only performance but also simply to load the module, otherwise
39
  // dlopen results in Invalid argument.
40
967
  int mode = RTLD_LAZY;
41
967
  if (load_globally) {
42
1
    mode |= RTLD_GLOBAL;
43
966
  } else {
44
    // RTLD_LOCAL is used by default to avoid collisions between multiple modules.
45
966
    mode |= RTLD_LOCAL;
46
966
  }
47
967
  if (do_not_close) {
48
33
    mode |= RTLD_NODELETE;
49
33
  }
50
967
  handle = dlopen(object_file_absolute_path.c_str(), mode);
51
967
  if (handle == nullptr) {
52
18
    return absl::InvalidArgumentError(absl::StrCat(
53
18
        "Failed to load dynamic module: ", object_file_absolute_path.c_str(), " : ", dlerror()));
54
18
  }
55

            
56
949
  DynamicModulePtr dynamic_module = std::make_unique<DynamicModule>(handle);
57

            
58
949
  const auto init_function =
59
949
      dynamic_module->getFunctionPointer<decltype(&envoy_dynamic_module_on_program_init)>(
60
949
          "envoy_dynamic_module_on_program_init");
61

            
62
949
  if (!init_function.ok()) {
63
2
    return init_function.status();
64
2
  }
65

            
66
947
  const char* abi_version = (*init_function.value())();
67
947
  if (abi_version == nullptr) {
68
2
    return absl::InvalidArgumentError(
69
2
        absl::StrCat("Failed to initialize dynamic module: ", object_file_absolute_path.c_str()));
70
2
  }
71
  // We log a warning if the ABI version does not match exactly.
72
945
  if (absl::string_view(abi_version) != absl::string_view(ENVOY_DYNAMIC_MODULES_ABI_VERSION)) {
73
2
    ENVOY_LOG_TO_LOGGER(
74
2
        Envoy::Logger::Registry::getLog(Envoy::Logger::Id::dynamic_modules), warn,
75
2
        "Dynamic module ABI version {} is deprecated. Please recompile the module against the "
76
2
        "SDK with the exact Envoy version used by the main program.",
77
2
        abi_version);
78
943
  } else {
79
943
    ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::dynamic_modules), info,
80
943
                        "Dynamic module ABI version {} matched.", abi_version);
81
943
  }
82
945
  return dynamic_module;
83
947
}
84

            
85
absl::StatusOr<DynamicModulePtr> newDynamicModuleByName(const absl::string_view module_name,
86
                                                        const bool do_not_close,
87
431
                                                        const bool load_globally) {
88
  // Probe for the module's init symbol with the module name as a prefix. If the symbol is found
89
  // in the process binary (via dlsym(RTLD_DEFAULT)), treat this as a statically linked module.
90
431
  const std::string static_init_symbol =
91
431
      absl::StrCat(module_name, "_envoy_dynamic_module_on_program_init");
92
431
  if (dlsym(RTLD_DEFAULT, static_init_symbol.c_str()) != nullptr) {
93
24
    return newStaticModule(module_name);
94
24
  }
95
  // First, try ENVOY_DYNAMIC_MODULES_SEARCH_PATH which falls back to the current directory.
96
407
  const char* module_search_path = getenv(DYNAMIC_MODULES_SEARCH_PATH);
97
407
  if (!module_search_path) {
98
2
    module_search_path = ".";
99
2
  }
100
407
  const std::filesystem::path file_path =
101
407
      std::filesystem::path(module_search_path) / fmt::format("lib{}.so", module_name);
102
407
  const std::filesystem::path file_path_absolute = std::filesystem::absolute(file_path);
103
407
  if (std::filesystem::exists(file_path_absolute)) {
104
392
    absl::StatusOr<DynamicModulePtr> dynamic_module =
105
392
        newDynamicModule(file_path_absolute, do_not_close, load_globally);
106
    // If the file exists but failed to load, return the error without trying other paths.
107
    // This allows the user to get the detailed error message such as missing dependencies, ABI
108
    // mismatch, etc.
109
392
    return dynamic_module;
110
392
  }
111

            
112
  // If not found, pass only the library name to dlopen to search in the standard library paths.
113
  // The man page of dlopen(3) says:
114
  //
115
  // > If path contains a slash ("/"), then it is interpreted as a
116
  // > (relative or absolute) pathname. Otherwise, the dynamic linker
117
  // > searches for the object ...
118
  //
119
  // which basically says dlopen searches for LD_LIBRARY_PATH and /usr/lib, etc.
120
15
  absl::StatusOr<DynamicModulePtr> dynamic_module =
121
15
      newDynamicModule(fmt::format("lib{}.so", module_name), do_not_close, load_globally);
122
15
  if (dynamic_module.ok()) {
123
1
    return dynamic_module;
124
1
  }
125

            
126
14
  return absl::InvalidArgumentError(
127
14
      absl::StrCat("Failed to load dynamic module: lib", module_name,
128
14
                   ".so not found in any search path: ", file_path_absolute.c_str(),
129
14
                   " or standard library paths such as LD_LIBRARY_PATH, /usr/lib, etc. or failed "
130
14
                   "to initialize: ",
131
14
                   dynamic_module.status().message()));
132
15
}
133

            
134
1052
DynamicModule::~DynamicModule() {
135
1052
  if (!static_module_name_.empty()) {
136
    // Static modules have no dlopen handle to close.
137
26
    return;
138
26
  }
139
1026
  dlclose(handle_);
140
1026
}
141

            
142
15081
void* DynamicModule::getSymbol(const absl::string_view symbol_ref) const {
143
15081
  if (!static_module_name_.empty()) {
144
    // For statically linked modules, look up the prefixed symbol in the process binary.
145
616
    const std::string prefixed = absl::StrCat(static_module_name_, "_", symbol_ref);
146
616
    return dlsym(RTLD_DEFAULT, prefixed.c_str());
147
616
  }
148
  // TODO(mathetake): maybe we should accept null-terminated const char* instead of string_view to
149
  // avoid unnecessary copy because it is likely that this is only called for a constant string,
150
  // though this is not a performance critical path.
151
14465
  return dlsym(handle_, std::string(symbol_ref).c_str());
152
15081
}
153

            
154
23
std::filesystem::path moduleTempPath(const absl::string_view sha256) {
155
23
  return std::filesystem::temp_directory_path() / fmt::format("envoy_dynamic_module_{}.so", sha256);
156
23
}
157

            
158
absl::StatusOr<DynamicModulePtr> newDynamicModuleFromBytes(const absl::string_view module_bytes,
159
                                                           const absl::string_view sha256,
160
                                                           const bool do_not_close,
161
9
                                                           const bool load_globally) {
162
9
  std::filesystem::path temp_file_path = moduleTempPath(sha256);
163

            
164
  // Write the (already SHA256-verified) bytes to a staging file, then atomically rename.
165
  // If the module was already loaded at this path, newDynamicModule's RTLD_NOLOAD check
166
  // returns the existing handle without re-init.
167
9
  std::string staging_template = temp_file_path.string() + ".XXXXXX";
168
9
  int fd = mkstemp(staging_template.data());
169
9
  if (fd == -1) {
170
    return absl::InternalError(absl::StrCat(
171
        "Failed to create temporary staging file for dynamic module: ", staging_template, ": ",
172
        errorDetails(errno)));
173
  }
174

            
175
9
  size_t total_written = 0;
176
18
  while (total_written < module_bytes.size()) {
177
9
    ssize_t written =
178
9
        write(fd, module_bytes.data() + total_written, module_bytes.size() - total_written);
179
9
    if (written < 0) {
180
      if (errno == EINTR) {
181
        continue;
182
      }
183
      close(fd);
184
      std::filesystem::remove(staging_template);
185
      return absl::InternalError(
186
          absl::StrCat("Failed to write to staging file for dynamic module: ", staging_template));
187
    }
188
9
    total_written += written;
189
9
  }
190
9
  close(fd);
191

            
192
9
  std::filesystem::path staging_path(staging_template);
193
9
  std::filesystem::permissions(staging_path, std::filesystem::perms::owner_all,
194
9
                               std::filesystem::perm_options::replace);
195
9
  std::filesystem::rename(staging_path, temp_file_path);
196
9
  auto result = newDynamicModule(temp_file_path, do_not_close, load_globally);
197
9
  if (!result.ok()) {
198
    // Clean up the invalid file.
199
2
    std::filesystem::remove(temp_file_path);
200
2
  }
201
9
  return result;
202
9
}
203

            
204
26
absl::StatusOr<DynamicModulePtr> newStaticModule(const absl::string_view module_name) {
205
26
  auto dynamic_module = std::make_unique<DynamicModule>(module_name);
206

            
207
26
  const auto init_function =
208
26
      dynamic_module->getFunctionPointer<decltype(&envoy_dynamic_module_on_program_init)>(
209
26
          "envoy_dynamic_module_on_program_init");
210
26
  if (!init_function.ok()) {
211
1
    return init_function.status();
212
1
  }
213

            
214
25
  const char* abi_version = (*init_function.value())();
215
25
  if (abi_version == nullptr) {
216
    return absl::InvalidArgumentError(
217
        absl::StrCat("Failed to initialize static module: ", module_name));
218
  }
219
25
  if (absl::string_view(abi_version) != absl::string_view(ENVOY_DYNAMIC_MODULES_ABI_VERSION)) {
220
    ENVOY_LOG_TO_LOGGER(
221
        Envoy::Logger::Registry::getLog(Envoy::Logger::Id::dynamic_modules), warn,
222
        "Static module ABI version {} is deprecated. Please recompile the module against the "
223
        "SDK with the exact Envoy version used by the main program.",
224
        abi_version);
225
25
  } else {
226
25
    ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::dynamic_modules), info,
227
25
                        "Static module ABI version {} matched.", abi_version);
228
25
  }
229
25
  return dynamic_module;
230
25
}
231

            
232
} // namespace DynamicModules
233
} // namespace Extensions
234
} // namespace Envoy