|
1 | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
2 | 2 | // Licensed under the MIT License. |
3 | 3 |
|
| 4 | +#include <vector> |
| 5 | + |
4 | 6 | #include "core/common/common.h" |
5 | 7 |
|
6 | | -#if !defined(__ANDROID__) && !defined(__wasm__) && !defined(_OPSCHEMA_LIB_) && !defined(_AIX) |
7 | | -#include <execinfo.h> |
| 8 | +#if !defined(NDEBUG) && !defined(__ANDROID__) && !defined(__wasm__) && !defined(_OPSCHEMA_LIB_) && !defined(_AIX) |
| 9 | +#include <dlfcn.h> |
| 10 | +#include <fcntl.h> |
| 11 | +#include <spawn.h> |
| 12 | +#include <sys/wait.h> |
| 13 | +#include <unistd.h> |
| 14 | +#include <cerrno> |
| 15 | +#include <cinttypes> |
| 16 | +#include <climits> |
| 17 | +#include <cstdio> |
| 18 | +#include <cstdlib> |
| 19 | +#include <cstring> |
| 20 | +#include <optional> |
| 21 | +#include <sstream> |
| 22 | +#include <string> |
| 23 | +#include <algorithm> |
| 24 | +#include <unordered_map> |
| 25 | +#include "absl/debugging/stacktrace.h" |
| 26 | +#include "absl/debugging/symbolize.h" |
| 27 | +#include "core/platform/scoped_resource.h" |
| 28 | + |
| 29 | +#ifdef __APPLE__ |
| 30 | +#include <crt_externs.h> |
| 31 | +#define environ (*_NSGetEnviron()) |
| 32 | +#else |
| 33 | +extern char** environ; |
8 | 34 | #endif |
9 | | -#include <vector> |
| 35 | + |
| 36 | +namespace { |
| 37 | + |
| 38 | +// Traits for a POSIX file descriptor, used with onnxruntime::ScopedResource to |
| 39 | +// close it on scope exit unless released. |
| 40 | +struct FdTraits { |
| 41 | + using Handle = int; |
| 42 | + static Handle GetInvalidHandleValue() noexcept { return -1; } |
| 43 | + static void CleanUp(Handle fd) noexcept { close(fd); } |
| 44 | +}; |
| 45 | +using ScopedFd = onnxruntime::ScopedResource<FdTraits>; |
| 46 | + |
| 47 | +// Traits for a FILE* obtained from fdopen, used with onnxruntime::ScopedResource |
| 48 | +// to fclose it on scope exit. |
| 49 | +struct FileTraits { |
| 50 | + using Handle = FILE*; |
| 51 | + static Handle GetInvalidHandleValue() noexcept { return nullptr; } |
| 52 | + static void CleanUp(Handle fp) noexcept { fclose(fp); } |
| 53 | +}; |
| 54 | +using ScopedFile = onnxruntime::ScopedResource<FileTraits>; |
| 55 | + |
| 56 | +// RAII wrapper for posix_spawn_file_actions_t. |
| 57 | +class ScopedSpawnFileActions { |
| 58 | + public: |
| 59 | + ScopedSpawnFileActions() = default; |
| 60 | + ScopedSpawnFileActions(const ScopedSpawnFileActions&) = delete; |
| 61 | + ScopedSpawnFileActions& operator=(const ScopedSpawnFileActions&) = delete; |
| 62 | + ~ScopedSpawnFileActions() { |
| 63 | + if (initialized_) { |
| 64 | + posix_spawn_file_actions_destroy(&actions_); |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + int Init() { |
| 69 | + const int rc = posix_spawn_file_actions_init(&actions_); |
| 70 | + initialized_ = (rc == 0); |
| 71 | + return rc; |
| 72 | + } |
| 73 | + posix_spawn_file_actions_t* get() { return &actions_; } |
| 74 | + |
| 75 | + private: |
| 76 | + posix_spawn_file_actions_t actions_{}; |
| 77 | + bool initialized_ = false; |
| 78 | +}; |
| 79 | + |
| 80 | +// RAII wrapper that reaps a child process on scope exit and exposes its status. |
| 81 | +class ScopedChild { |
| 82 | + public: |
| 83 | + explicit ScopedChild(pid_t pid) : pid_(pid) {} |
| 84 | + ScopedChild(const ScopedChild&) = delete; |
| 85 | + ScopedChild& operator=(const ScopedChild&) = delete; |
| 86 | + ~ScopedChild() { Wait(); } |
| 87 | + |
| 88 | + // Wait for the child and cache its exit status. Safe to call more than once. |
| 89 | + void Wait() { |
| 90 | + if (pid_ == -1) { |
| 91 | + return; |
| 92 | + } |
| 93 | + int status = 0; |
| 94 | + while (waitpid(pid_, &status, 0) == -1 && errno == EINTR) { |
| 95 | + } |
| 96 | + status_ = status; |
| 97 | + pid_ = -1; |
| 98 | + } |
| 99 | + |
| 100 | + // Returns true only if the child exited normally with code 0. |
| 101 | + bool ExitedCleanly() { |
| 102 | + Wait(); |
| 103 | + return status_.has_value() && WIFEXITED(*status_) && WEXITSTATUS(*status_) == 0; |
| 104 | + } |
| 105 | + |
| 106 | + private: |
| 107 | + pid_t pid_; |
| 108 | + std::optional<int> status_; |
| 109 | +}; |
| 110 | + |
| 111 | +// ORT_ADDR2LINE controls optional source file:line resolution for stack frames. |
| 112 | +// It is read as a non-negative integer N: |
| 113 | +// - unset or 0 : disabled (default). Only symbol names from absl::Symbolize |
| 114 | +// are shown and no subprocess is spawned. |
| 115 | +// - N > 0 : resolve file:line for the top N frames by invoking the |
| 116 | +// external `addr2line` tool (part of binutils). This is opt-in |
| 117 | +// because spawning addr2line and parsing DWARF can be very slow |
| 118 | +// on large debug binaries, and addr2line may not be installed. |
| 119 | +inline int GetAddr2LineEnv() { |
| 120 | + const char* val = std::getenv("ORT_ADDR2LINE"); |
| 121 | + if (val == nullptr) { |
| 122 | + return 0; |
| 123 | + } |
| 124 | + char* end = nullptr; |
| 125 | + long parsed = strtol(val, &end, 10); |
| 126 | + // Reject empty input and any value with trailing non-numeric characters |
| 127 | + // (e.g. "10foo"), so a malformed setting disables resolution rather than |
| 128 | + // silently using a partially parsed number. |
| 129 | + if (end == val || *end != '\0' || parsed < 0 || parsed > INT_MAX) { |
| 130 | + return 0; |
| 131 | + } |
| 132 | + return static_cast<int>(parsed); |
| 133 | +} |
| 134 | + |
| 135 | +int GetAddr2LineCount() { |
| 136 | + static const int count = GetAddr2LineEnv(); |
| 137 | + return count; |
| 138 | +} |
| 139 | + |
| 140 | +bool CreateCloseOnExecPipe(int pipe_fds[2]) { |
| 141 | + if (pipe(pipe_fds) != 0) { |
| 142 | + return false; |
| 143 | + } |
| 144 | + |
| 145 | + for (int i = 0; i < 2; ++i) { |
| 146 | + const int flags = fcntl(pipe_fds[i], F_GETFD); |
| 147 | + if (flags == -1 || fcntl(pipe_fds[i], F_SETFD, flags | FD_CLOEXEC) == -1) { |
| 148 | + close(pipe_fds[0]); |
| 149 | + close(pipe_fds[1]); |
| 150 | + return false; |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + return true; |
| 155 | +} |
| 156 | + |
| 157 | +// Resolve file offsets to file:line using addr2line, grouped by binary. |
| 158 | +// Returns a map from original address to "file:line" string. |
| 159 | +std::unordered_map<void*, std::string> ResolveWithAddr2Line( |
| 160 | + void** addresses, int depth) { |
| 161 | + std::unordered_map<void*, std::string> result; |
| 162 | + |
| 163 | + // Group addresses by their containing binary/shared-object. |
| 164 | + // Key: binary path, Value: vector of (address, file_offset) |
| 165 | + struct FrameInfo { |
| 166 | + void* addr; |
| 167 | + uintptr_t offset; |
| 168 | + }; |
| 169 | + std::unordered_map<std::string, std::vector<FrameInfo>> groups; |
| 170 | + |
| 171 | + for (int i = 0; i < depth; ++i) { |
| 172 | + Dl_info info; |
| 173 | + if (dladdr(addresses[i], &info) && info.dli_fname) { |
| 174 | + const uintptr_t addr = reinterpret_cast<uintptr_t>(addresses[i]); |
| 175 | + const uintptr_t base = reinterpret_cast<uintptr_t>(info.dli_fbase); |
| 176 | + // Skip frames whose address is at or below the module base. Subtracting |
| 177 | + // below would underflow uintptr_t and feed a garbage offset to addr2line. |
| 178 | + if (addr <= base) { |
| 179 | + continue; |
| 180 | + } |
| 181 | + // Each captured address is a return address pointing just past the call |
| 182 | + // instruction. Subtract 1 so addr2line resolves the call site itself |
| 183 | + // rather than the following statement (which may be a different source |
| 184 | + // line or even the next function). The map is still keyed by the original |
| 185 | + // address so it matches the absl::Symbolize lookup in GetStackTrace. |
| 186 | + uintptr_t offset = addr - 1 - base; |
| 187 | + groups[info.dli_fname].push_back({addresses[i], offset}); |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + // Call addr2line once per binary with all offsets in batch. |
| 192 | + // Use posix_spawnp + pipe instead of popen to avoid shell injection risks |
| 193 | + // from untrusted binary paths (e.g., paths with spaces or special chars). |
| 194 | + for (const auto& [binary, frames] : groups) { |
| 195 | + // Build argv: {"addr2line", "-e", binary, "0xoffset1", "0xoffset2", ..., nullptr} |
| 196 | + std::vector<std::string> offset_strs; |
| 197 | + offset_strs.reserve(frames.size()); |
| 198 | + for (const auto& frame : frames) { |
| 199 | + char buf[32]; |
| 200 | + snprintf(buf, sizeof(buf), "0x%" PRIxPTR, frame.offset); |
| 201 | + offset_strs.emplace_back(buf); |
| 202 | + } |
| 203 | + |
| 204 | + // Build raw argv array for addr2line. All pointers reference stable strings above. |
| 205 | + std::vector<char*> argv; |
| 206 | + argv.reserve(3 + frames.size() + 1); |
| 207 | + argv.push_back(const_cast<char*>("addr2line")); |
| 208 | + argv.push_back(const_cast<char*>("-e")); |
| 209 | + argv.push_back(const_cast<char*>(binary.c_str())); |
| 210 | + for (auto& s : offset_strs) { |
| 211 | + argv.push_back(const_cast<char*>(s.c_str())); |
| 212 | + } |
| 213 | + argv.push_back(nullptr); |
| 214 | + |
| 215 | + // Create a pipe for reading addr2line's stdout. RAII wrappers below own the |
| 216 | + // descriptors, spawn file actions, FILE*, and child process so each early |
| 217 | + // 'continue' releases everything without manual cleanup calls. |
| 218 | + int pipe_fds[2]; |
| 219 | + if (!CreateCloseOnExecPipe(pipe_fds)) continue; |
| 220 | + ScopedFd read_fd(pipe_fds[0]); |
| 221 | + ScopedFd write_fd(pipe_fds[1]); |
| 222 | + |
| 223 | + ScopedSpawnFileActions actions; |
| 224 | + if (actions.Init() != 0) continue; |
| 225 | + |
| 226 | + if (posix_spawn_file_actions_adddup2(actions.get(), write_fd.Get(), STDOUT_FILENO) != 0 || |
| 227 | + posix_spawn_file_actions_addclose(actions.get(), read_fd.Get()) != 0 || |
| 228 | + // Redirect stderr to /dev/null to suppress error messages. |
| 229 | + posix_spawn_file_actions_addopen(actions.get(), STDERR_FILENO, "/dev/null", O_WRONLY, 0) != 0) { |
| 230 | + continue; |
| 231 | + } |
| 232 | + |
| 233 | + pid_t pid = -1; |
| 234 | + const int spawn_ret = posix_spawnp(&pid, "addr2line", actions.get(), nullptr, argv.data(), environ); |
| 235 | + |
| 236 | + // Close the write end in the parent so we observe EOF once addr2line exits. |
| 237 | + write_fd.Reset(); |
| 238 | + |
| 239 | + // If addr2line cannot be executed (e.g. not installed), glibc posix_spawnp |
| 240 | + // reports the failure here with a non-zero return (ENOENT) and no child is |
| 241 | + // created. Some implementations instead spawn a child that exits 127; that |
| 242 | + // case is caught by the ExitedCleanly() check below, which discards output. |
| 243 | + if (spawn_ret != 0) continue; |
| 244 | + ScopedChild child(pid); |
| 245 | + |
| 246 | + FILE* raw_fp = fdopen(read_fd.Get(), "r"); |
| 247 | + if (raw_fp == nullptr) continue; |
| 248 | + read_fd.Release(); // fp now owns the descriptor. |
| 249 | + ScopedFile fp(raw_fp); |
| 250 | + |
| 251 | + // Read into a local map and only commit it if addr2line exited cleanly, so a |
| 252 | + // failed run (e.g., addr2line missing, or an unreadable binary) contributes |
| 253 | + // no partial or garbage results. |
| 254 | + std::unordered_map<void*, std::string> binary_result; |
| 255 | + |
| 256 | + // We intentionally do not use -f/-i so output remains 1 line per address. |
| 257 | + // Adding those flags would break the 1:1 parsing below. |
| 258 | + char line_buf[1024]; |
| 259 | + size_t frame_idx = 0; |
| 260 | + while (fgets(line_buf, sizeof(line_buf), fp.Get()) && frame_idx < frames.size()) { |
| 261 | + // Remove trailing newline |
| 262 | + size_t len = strlen(line_buf); |
| 263 | + if (len > 0 && line_buf[len - 1] == '\n') line_buf[len - 1] = '\0'; |
| 264 | + |
| 265 | + std::string resolved(line_buf); |
| 266 | + // addr2line returns "??:0" or "??:?" for unknown frames, so skip those. |
| 267 | + if (resolved != "??:0" && resolved != "??:?" && resolved.substr(0, 2) != "??") { |
| 268 | + binary_result[frames[frame_idx].addr] = std::move(resolved); |
| 269 | + } |
| 270 | + ++frame_idx; |
| 271 | + } |
| 272 | + |
| 273 | + // Reap the child and only trust the output if it exited with status 0. |
| 274 | + if (child.ExitedCleanly()) { |
| 275 | + for (auto& kv : binary_result) { |
| 276 | + result.insert(std::move(kv)); |
| 277 | + } |
| 278 | + } |
| 279 | + } |
| 280 | + |
| 281 | + return result; |
| 282 | +} |
| 283 | + |
| 284 | +} // namespace |
| 285 | +#endif // !defined(NDEBUG) && !defined(__ANDROID__) && !defined(__wasm__) && !defined(_OPSCHEMA_LIB_) && !defined(_AIX) |
10 | 286 |
|
11 | 287 | namespace onnxruntime { |
12 | 288 |
|
13 | 289 | std::vector<std::string> GetStackTrace() { |
14 | 290 | std::vector<std::string> stack; |
15 | 291 |
|
16 | | -#if !defined(NDEBUG) && !defined(__ANDROID__) && !defined(__wasm__) && !defined(_OPSCHEMA_LIB_) |
17 | | - constexpr int kCallstackLimit = 64; // Maximum depth of callstack |
18 | | - |
19 | | - void* array[kCallstackLimit]; |
20 | | - char** strings = nullptr; |
| 292 | +#if !defined(NDEBUG) && !defined(__ANDROID__) && !defined(__wasm__) && !defined(_OPSCHEMA_LIB_) && !defined(_AIX) |
| 293 | + constexpr int kCallstackLimit = 64; |
| 294 | + void* addresses[kCallstackLimit]; |
21 | 295 |
|
22 | | - int size = backtrace(array, kCallstackLimit); |
23 | | - stack.reserve(size); |
24 | | - strings = backtrace_symbols(array, size); |
| 296 | + // skip_count=2 hides GetStackTrace and its caller (ORT_ENFORCE macro internals) |
| 297 | + int depth = absl::GetStackTrace(addresses, kCallstackLimit, /*skip_count=*/2); |
| 298 | + stack.reserve(depth); |
25 | 299 |
|
26 | | - // NOTE: To get meaningful info from the output, addr2line (or atos on osx) would need to be used. |
27 | | - // See https://gist.github.com/jvranish/4441299 for an example. |
28 | | - // |
29 | | - // To manually translate the output, use the value in the '()' after the executable name with addr2line |
30 | | - // e.g. |
31 | | - // Stacktrace: |
32 | | - // /home/me/src/github/onnxruntime/build/Linux/Debug/onnxruntime_test_all(+0x3f46cc) [0x559543faf6cc] |
33 | | - // |
34 | | - // >addr2line -f -C -e /home/me/src/github/onnxruntime/build/Linux/Debug/onnxruntime_test_all +0x3f46cc |
35 | | - |
36 | | - // hide GetStackTrace so the output starts with the 'real' location |
37 | | - constexpr int start_frame = 1; |
38 | | - for (int i = start_frame; i < size; i++) { |
39 | | - stack.push_back(strings[i]); |
| 300 | + // Resolve file:line via addr2line only when explicitly opted in via ORT_ADDR2LINE=*, |
| 301 | + // since it spawns external processes and can be very slow on large debug binaries. |
| 302 | + std::unordered_map<void*, std::string> resolved; |
| 303 | + int count = GetAddr2LineCount(); |
| 304 | + if (count > 0) { |
| 305 | + // Only resolve the top N frames to limit addr2line overhead on large binaries. |
| 306 | + resolved = ResolveWithAddr2Line(addresses, std::min(depth, count)); |
40 | 307 | } |
41 | 308 |
|
42 | | - free(strings); |
| 309 | + for (int i = 0; i < depth; ++i) { |
| 310 | + std::ostringstream oss; |
| 311 | + char symbol[1024]; |
| 312 | + if (absl::Symbolize(addresses[i], symbol, sizeof(symbol))) { |
| 313 | + oss << symbol; |
| 314 | + } else { |
| 315 | + oss << "[unknown]"; |
| 316 | + } |
43 | 317 |
|
44 | | -#endif |
| 318 | + auto it = resolved.find(addresses[i]); |
| 319 | + if (it != resolved.end()) { |
| 320 | + oss << " at " << it->second; |
| 321 | + } else { |
| 322 | + oss << " [" << addresses[i] << "]"; |
| 323 | + } |
| 324 | + stack.push_back(oss.str()); |
| 325 | + } |
| 326 | +#endif // !defined(NDEBUG) && !defined(__ANDROID__) && !defined(__wasm__) && !defined(_OPSCHEMA_LIB_) && !defined(_AIX) |
45 | 327 |
|
46 | 328 | return stack; |
47 | 329 | } |
|
0 commit comments