diff --git a/CHANGELOG.md b/CHANGELOG.md index 8620e2d55..0dc723ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Native: Provide exception metadata, registers, and a basic crashing-thread stack to `on_crash` for crash identification. ([#2174](https://github.com/getsentry/sentry-native/pull/2174)) - Native: reduce disk I/O on scope changes by sending incremental updates to the crash daemon over IPC. ([#2155](https://github.com/getsentry/sentry-native/pull/2155), [#2157](https://github.com/getsentry/sentry-native/pull/2157)) - Native: install handlers for the daemon's own fatal signals and unhandled exceptions, allowing best-effort recovery actions such as dumping pending envelopes before termination. ([#2220](https://github.com/getsentry/sentry-native/pull/2220)) +- Report the host OS in the OS context under Wine and Proton, instead of the Windows version that Wine emulates. On Linux, prefer the host `os-release` inside Steam Runtime and Flatpak containers, and report SteamOS and Bazzite as the OS name. ([#2241](https://github.com/getsentry/sentry-native/pull/2241)) - Add `sentry_scope_set_trace` for per-scope traces. ([#2236](https://github.com/getsentry/sentry-native/pull/2236)) - Add `sentry_continue_trace` to continue incoming traces without a transaction. ([#2237](https://github.com/getsentry/sentry-native/pull/2237)) - Add `sentry_iter_trace_propagation_headers` to propagate traces without a span. ([#2240](https://github.com/getsentry/sentry-native/pull/2240)) diff --git a/src/sentry_os.c b/src/sentry_os.c index b6296dad7..5248d42ec 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -6,6 +6,7 @@ #include "sentry_value.h" #include +#include #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) # include "sentry_core.h" # include "sentry_utils.h" @@ -37,6 +38,166 @@ sentry__process_vm_readv(pid_t pid, const struct iovec *local_iov, } #endif +#if (defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX)) \ + || (defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_MACOS) \ + && !defined(SENTRY_PLATFORM_PS)) +static bool +set_os_uname(sentry_value_t os, const char *sysname, const char *release) +{ + char *version = sentry__string_clone(release); + if (!version) { + return false; + } + + char *build = version; + size_t num_dots = 0; + for (; build[0] != '\0'; build++) { + char c = build[0]; + if (c == '.') { + num_dots += 1; + } + if (!(c >= '0' && c <= '9') && (c != '.' || num_dots > 2)) { + break; + } + } + char *build_start = build; + if (build[0] == '-' || build[0] == '.') { + build_start++; + } + + if (build_start[0] != '\0') { + sentry_value_set_by_key( + os, "build", sentry_value_new_string(build_start)); + } + + build[0] = '\0'; + + sentry_value_set_by_key(os, "name", sentry_value_new_string(sysname)); + sentry_value_set_by_key(os, "version", sentry_value_new_string(version)); + sentry_free(version); + return true; +} +#endif + +#if defined(SENTRY_PLATFORM_LINUX) \ + || (defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX)) +# define OS_RELEASE_MAX_LINE_SIZE 256 +# define OS_RELEASE_MAX_KEY_SIZE 64 +# define OS_RELEASE_MAX_VALUE_SIZE 128 + +// Prefer the host over the Steam Runtime or Flatpak container. +static const char *const os_release_paths[] = { + "/run/host/etc/os-release", + "/run/host/usr/lib/os-release", + "/run/host/os-release", + "/etc/os-release", + "/usr/lib/os-release", +}; + +static int +parse_os_release_line(const char *line, char *key, char *value) +{ + const char *equals = strchr(line, '='); + if (equals == NULL) + return 1; + + size_t key_length + = MIN((size_t)(equals - line), OS_RELEASE_MAX_KEY_SIZE - 1); + strncpy(key, line, key_length); + key[key_length] = '\0'; + + sentry_slice_t value_slice + = { .ptr = equals + 1, .len = strlen(equals + 1) }; + + // some values are wrapped in double quotes + if (value_slice.len >= 2 && value_slice.ptr[0] == '\"' + && value_slice.ptr[value_slice.len - 1] == '\"') { + value_slice.ptr++; + value_slice.len -= 2; + } + + sentry__slice_to_buffer(value_slice, value, OS_RELEASE_MAX_VALUE_SIZE); + + return 0; +} + +static void +parse_line_into_object(const char *line, sentry_value_t os_dist) +{ + char value[OS_RELEASE_MAX_VALUE_SIZE]; + char key[OS_RELEASE_MAX_KEY_SIZE]; + + if (parse_os_release_line(line, key, value) == 0) { + if (strcmp(key, "ID") == 0) { + sentry_value_set_by_key( + os_dist, "name", sentry_value_new_string(value)); + } + + if (strcmp(key, "VERSION_ID") == 0) { + sentry_value_set_by_key( + os_dist, "version", sentry_value_new_string(value)); + } + + if (strcmp(key, "PRETTY_NAME") == 0) { + sentry_value_set_by_key( + os_dist, "pretty_name", sentry_value_new_string(value)); + } + + if (strcmp(key, "NAME") == 0) { + sentry_value_set_by_key( + os_dist, "display_name", sentry_value_new_string(value)); + } + + if (strcmp(key, "BUILD_ID") == 0) { + sentry_value_set_by_key( + os_dist, "build", sentry_value_new_string(value)); + } + } +} + +# ifndef SENTRY_UNITTEST +static +# endif + void + set_os_distribution( + sentry_value_t os, sentry_value_t os_dist, const char *release) +{ + sentry_value_t id = sentry_value_get_by_key(os_dist, "name"); + const char *id_str = sentry_value_as_string(id); + + // SteamOS and Bazzite are reported as the OS itself, not as a distribution. + if (strcmp(id_str, "steamos") == 0 || strcmp(id_str, "bazzite") == 0) { + sentry_value_t name = sentry_value_get_by_key(os_dist, "display_name"); + if (sentry_value_is_null(name)) { + name = id; + } + sentry_value_t version = sentry_value_get_by_key(os_dist, "version"); + if (!sentry_value_is_null(version)) { + sentry_value_incref(version); + sentry_value_set_by_key(os, "version", version); + } + sentry_value_set_by_key(os, "name", name); + sentry_value_set_by_key( + os, "build", sentry_value_get_by_key(os_dist, "build")); + sentry_value_set_by_key( + os, "kernel_version", sentry_value_new_string(release)); + sentry_value_incref(name); + sentry_value_incref(sentry_value_get_by_key(os_dist, "build")); + return; + } + + sentry_value_set_by_key( + os, "distribution_name", sentry_value_get_by_key(os_dist, "name")); + sentry_value_set_by_key(os, "distribution_version", + sentry_value_get_by_key(os_dist, "version")); + sentry_value_set_by_key(os, "distribution_pretty_name", + sentry_value_get_by_key(os_dist, "pretty_name")); + sentry_value_incref(sentry_value_get_by_key(os_dist, "name")); + sentry_value_incref(sentry_value_get_by_key(os_dist, "version")); + sentry_value_incref(sentry_value_get_by_key(os_dist, "pretty_name")); +} +#endif + #ifdef SENTRY_PLATFORM_WINDOWS # include @@ -402,11 +563,96 @@ sentry__get_wine_context(void) return context; } +static sentry_value_t +get_wine_os_release(const char *path) +{ + sentry_path_t *wine_path = make_wine_path(path, ""); + char *buf = read_wine_file(wine_path); + sentry__path_free(wine_path); + if (!buf) { + return sentry_value_new_null(); + } + + sentry_value_t os_dist = sentry_value_new_object(); + for (char *line = buf; line && *line;) { + char *next = strchr(line, '\n'); + if (next) { + *next++ = '\0'; + } + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\r') { + line[len - 1] = '\0'; + } + parse_line_into_object(line, os_dist); + line = next; + } + sentry_free(buf); + return os_dist; +} + +typedef void(CDECL *sentry__wine_get_host_version_t)( + const char **sysname, const char **release); + +/** + * Under Wine, reports the host OS instead of the Windows version that Wine + * emulates, in the same shape as the native Linux and macOS OS contexts. + */ +static sentry_value_t +get_wine_host_os_context(void) +{ + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (!ntdll) { + return sentry_value_new_null(); + } + + const sentry__wine_get_host_version_t wine_get_host_version + = (sentry__wine_get_host_version_t)GetProcAddress( + ntdll, "wine_get_host_version"); + if (!wine_get_host_version) { + return sentry_value_new_null(); + } + + const char *sysname = NULL; + const char *release = NULL; + wine_get_host_version(&sysname, &release); + if (sentry__string_empty(sysname) || sentry__string_empty(release)) { + return sentry_value_new_null(); + } + + sentry_value_t os = sentry_value_new_object(); + if (sentry_value_is_null(os) || !set_os_uname(os, sysname, release)) { + sentry_value_decref(os); + return sentry_value_new_null(); + } + + if (strcmp(sysname, "Linux") == 0) { + for (size_t i = 0; + i < sizeof(os_release_paths) / sizeof(os_release_paths[0]); i++) { + sentry_value_t os_dist = get_wine_os_release(os_release_paths[i]); + if (!sentry_value_is_null(os_dist)) { + set_os_distribution(os, os_dist, release); + sentry_value_decref(os_dist); + break; + } + } + } + + sentry_value_freeze(os); + return os; +} + # endif // !defined(SENTRY_PLATFORM_XBOX) sentry_value_t sentry__get_os_context(void) { +# if !defined(SENTRY_PLATFORM_XBOX) + sentry_value_t wine_os = get_wine_host_os_context(); + if (!sentry_value_is_null(wine_os)) { + return wine_os; + } +# endif + const sentry_value_t os = sentry_value_new_object(); if (sentry_value_is_null(os)) { return os; @@ -722,60 +968,6 @@ sentry__get_os_context(void) # include # if defined(SENTRY_PLATFORM_LINUX) -# define OS_RELEASE_MAX_LINE_SIZE 256 -# define OS_RELEASE_MAX_KEY_SIZE 64 -# define OS_RELEASE_MAX_VALUE_SIZE 128 - -static int -parse_os_release_line(const char *line, char *key, char *value) -{ - const char *equals = strchr(line, '='); - if (equals == NULL) - return 1; - - unsigned long key_length = MIN(equals - line, OS_RELEASE_MAX_KEY_SIZE - 1); - strncpy(key, line, key_length); - key[key_length] = '\0'; - - sentry_slice_t value_slice - = { .ptr = equals + 1, .len = strlen(equals + 1) }; - - // some values are wrapped in double quotes - if (value_slice.len >= 2 && value_slice.ptr[0] == '\"' - && value_slice.ptr[value_slice.len - 1] == '\"') { - value_slice.ptr++; - value_slice.len -= 2; - } - - sentry__slice_to_buffer(value_slice, value, OS_RELEASE_MAX_VALUE_SIZE); - - return 0; -} - -static void -parse_line_into_object(const char *line, sentry_value_t os_dist) -{ - char value[OS_RELEASE_MAX_VALUE_SIZE]; - char key[OS_RELEASE_MAX_KEY_SIZE]; - - if (parse_os_release_line(line, key, value) == 0) { - if (strcmp(key, "ID") == 0) { - sentry_value_set_by_key( - os_dist, "name", sentry_value_new_string(value)); - } - - if (strcmp(key, "VERSION_ID") == 0) { - sentry_value_set_by_key( - os_dist, "version", sentry_value_new_string(value)); - } - - if (strcmp(key, "PRETTY_NAME") == 0) { - sentry_value_set_by_key( - os_dist, "pretty_name", sentry_value_new_string(value)); - } - } -} - # ifndef SENTRY_UNITTEST static # endif @@ -849,60 +1041,20 @@ sentry__get_os_context(void) goto fail; } - char *build = uts.release; - size_t num_dots = 0; - for (; build[0] != '\0'; build++) { - char c = build[0]; - if (c == '.') { - num_dots += 1; - } - if (!(c >= '0' && c <= '9') && (c != '.' || num_dots > 2)) { - break; - } - } - char *build_start = build; - if (build[0] == '-' || build[0] == '.') { - build_start++; - } - - if (build_start[0] != '\0') { - sentry_value_set_by_key( - os, "build", sentry_value_new_string(build_start)); + if (!set_os_uname(os, uts.sysname, uts.release)) { + goto fail; } - build[0] = '\0'; - - sentry_value_set_by_key(os, "name", sentry_value_new_string(uts.sysname)); - sentry_value_set_by_key( - os, "version", sentry_value_new_string(uts.release)); - # if defined(SENTRY_PLATFORM_LINUX) - /** - * The file /etc/os-release takes precedence over /usr/lib/os-release. - * Applications should check for the former, and exclusively use its data if - * it exists, and only fall back to /usr/lib/os-release if it is missing. - * Applications should not read data from both files at the same time. - * - * From: - * https://www.freedesktop.org/software/systemd/man/latest/os-release.html#Description - */ - sentry_value_t os_dist = get_linux_os_release("/etc/os-release"); - if (sentry_value_is_null(os_dist)) { - os_dist = get_linux_os_release("/usr/lib/os-release"); - if (sentry_value_is_null(os_dist)) { - return os; + for (size_t i = 0; + i < sizeof(os_release_paths) / sizeof(os_release_paths[0]); i++) { + sentry_value_t os_dist = get_linux_os_release(os_release_paths[i]); + if (!sentry_value_is_null(os_dist)) { + set_os_distribution(os, os_dist, uts.release); + sentry_value_decref(os_dist); + break; } } - sentry_value_set_by_key( - os, "distribution_name", sentry_value_get_by_key(os_dist, "name")); - sentry_value_set_by_key(os, "distribution_version", - sentry_value_get_by_key(os_dist, "version")); - sentry_value_set_by_key(os, "distribution_pretty_name", - sentry_value_get_by_key(os_dist, "pretty_name")); - sentry_value_incref(sentry_value_get_by_key(os_dist, "name")); - sentry_value_incref(sentry_value_get_by_key(os_dist, "version")); - sentry_value_incref(sentry_value_get_by_key(os_dist, "pretty_name")); - sentry_value_decref(os_dist); # endif // defined(SENTRY_PLATFORM_LINUX) return os; diff --git a/tests/assertions.py b/tests/assertions.py index 864506094..1440efebf 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -13,7 +13,7 @@ import msgpack from . import REPLAY_ID, SENTRY_VERSION -from .conditions import is_android, is_asan, is_tsan, is_wine +from .conditions import is_android, is_asan, is_tsan VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)[-.]?(.*)") @@ -22,6 +22,31 @@ ) +OS_RELEASE_PATHS = [ + "/run/host/etc/os-release", + "/run/host/usr/lib/os-release", + "/run/host/os-release", + "/etc/os-release", + "/usr/lib/os-release", +] + + +def read_os_release(): + for path in OS_RELEASE_PATHS: + try: + with open(path) as f: + lines = f.read().splitlines() + except OSError: + continue + fields = {} + for line in lines: + key, sep, value = line.partition("=") + if sep: + fields[key] = value.strip('"') + return fields + return {} + + def matches(actual, expected): return {k: v for (k, v) in actual.items() if k in expected.keys()} == expected @@ -119,11 +144,11 @@ def assert_event_meta( if is_android: expected_sdk["name"] = "sentry.native.android" else: - if sys.platform == "win32" or is_wine: - expected_os_context = {"name": "Windows"} - if not is_wine: - expected_os_context["version"] = platform.version() - assert_matches(event["contexts"]["os"], expected_os_context) + if sys.platform == "win32": + assert_matches( + event["contexts"]["os"], + {"name": "Windows", "version": platform.version()}, + ) assert event["contexts"]["os"]["version"] is not None assert event["contexts"]["os"]["build"] is not None elif sys.platform == "linux": @@ -131,13 +156,26 @@ def assert_event_meta( match = VERSION_RE.match(version) version = match.group(1) build = match.group(2) - expected_os_context = {"name": "Linux", "version": version} - if build: - expected_os_context["build"] = build - - assert_matches(event["contexts"]["os"], expected_os_context) - assert "distribution_name" in event["contexts"]["os"] - assert "distribution_version" in event["contexts"]["os"] + os_release = read_os_release() + if os_release.get("ID") in ("steamos", "bazzite"): + expected_os_context = { + "name": os_release.get("NAME", os_release["ID"]), + "version": os_release.get("VERSION_ID", version), + "kernel_version": platform.release(), + } + if "BUILD_ID" in os_release: + expected_os_context["build"] = os_release["BUILD_ID"] + + assert_matches(event["contexts"]["os"], expected_os_context) + assert "distribution_name" not in event["contexts"]["os"] + else: + expected_os_context = {"name": "Linux", "version": version} + if build: + expected_os_context["build"] = build + + assert_matches(event["contexts"]["os"], expected_os_context) + assert "distribution_name" in event["contexts"]["os"] + assert "distribution_version" in event["contexts"]["os"] elif sys.platform == "darwin": version = platform.mac_ver()[0].split(".") if len(version) < 3: diff --git a/tests/unit/test_os.c b/tests/unit/test_os.c index 3c9e2a343..be8e64cc4 100644 --- a/tests/unit/test_os.c +++ b/tests/unit/test_os.c @@ -129,6 +129,24 @@ assert_equals_snap(sentry_value_t os_dist) } extern sentry_value_t get_linux_os_release(const char *os_rel_path); +extern void set_os_distribution( + sentry_value_t os, sentry_value_t os_dist, const char *release); + +static sentry_value_t +make_linux_os(void) +{ + sentry_value_t os = sentry_value_new_object(); + sentry_value_set_by_key(os, "name", sentry_value_new_string("Linux")); + sentry_value_set_by_key(os, "version", sentry_value_new_string("6.1.0")); + sentry_value_set_by_key(os, "build", sentry_value_new_string("test")); + return os; +} + +static const char * +os_string(sentry_value_t os, const char *key) +{ + return sentry_value_as_string(sentry_value_get_by_key(os, key)); +} #endif // defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) SENTRY_TEST(os_releases_snapshot) @@ -195,6 +213,63 @@ SENTRY_TEST(os_release_non_existent_files) #endif // !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) } +SENTRY_TEST(os_release_named_distributions) +{ +#if !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + SKIP_TEST(); +#else + sentry_value_t os = make_linux_os(); + sentry_value_t os_dist = sentry_value_new_object(); + sentry_value_set_by_key( + os_dist, "name", sentry_value_new_string("steamos")); + sentry_value_set_by_key( + os_dist, "display_name", sentry_value_new_string("SteamOS")); + sentry_value_set_by_key(os_dist, "version", sentry_value_new_string("3.7")); + sentry_value_set_by_key( + os_dist, "build", sentry_value_new_string("20250101.1")); + set_os_distribution(os, os_dist, "6.1.0-test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "name"), "SteamOS"); + TEST_CHECK_STRING_EQUAL(os_string(os, "version"), "3.7"); + TEST_CHECK_STRING_EQUAL(os_string(os, "build"), "20250101.1"); + TEST_CHECK_STRING_EQUAL(os_string(os, "kernel_version"), "6.1.0-test"); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(os, "distribution_name"))); + sentry_value_decref(os_dist); + sentry_value_decref(os); + + // falls back to ID and the kernel version, and drops the kernel build + os = make_linux_os(); + os_dist = sentry_value_new_object(); + sentry_value_set_by_key( + os_dist, "name", sentry_value_new_string("bazzite")); + set_os_distribution(os, os_dist, "6.1.0-test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "name"), "bazzite"); + TEST_CHECK_STRING_EQUAL(os_string(os, "version"), "6.1.0"); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key(os, "build"))); + TEST_CHECK_STRING_EQUAL(os_string(os, "kernel_version"), "6.1.0-test"); + sentry_value_decref(os_dist); + sentry_value_decref(os); + + os = make_linux_os(); + os_dist = sentry_value_new_object(); + sentry_value_set_by_key(os_dist, "name", sentry_value_new_string("ubuntu")); + sentry_value_set_by_key( + os_dist, "display_name", sentry_value_new_string("Ubuntu")); + sentry_value_set_by_key( + os_dist, "version", sentry_value_new_string("24.04")); + set_os_distribution(os, os_dist, "6.1.0-test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "name"), "Linux"); + TEST_CHECK_STRING_EQUAL(os_string(os, "version"), "6.1.0"); + TEST_CHECK_STRING_EQUAL(os_string(os, "build"), "test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "distribution_name"), "ubuntu"); + TEST_CHECK_STRING_EQUAL(os_string(os, "distribution_version"), "24.04"); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(os, "kernel_version"))); + sentry_value_decref(os_dist); + sentry_value_decref(os); +#endif // !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +} + #ifdef SENTRY_PLATFORM_WINDOWS extern BOOL(WINAPI *g_kernel32_SetThreadStackGuarantee)(PULONG); extern void(WINAPI *g_kernel32_GetCurrentThreadStackLimits)( diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 4bf1e6081..f6d7497cc 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -343,6 +343,7 @@ XX(options_thread_stackwalk_mode_clamp) XX(options_thread_stackwalk_mode_default) XX(options_thread_stackwalk_mode_set_get) XX(os) +XX(os_release_named_distributions) XX(os_release_non_existent_files) XX(os_releases_snapshot) XX(overflow_spans)