From 9286daf789f5e134a18b2704876a07efe45f465a Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:41:09 +0200 Subject: [PATCH 1/6] add reproducer for partial write fail Co-authored-by: Claude Opus 4.6 (1M context) --- tests/unit/test_envelopes.c | 74 +++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 2 files changed, 75 insertions(+) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 2f19c0450a..27daf5274c 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -12,6 +12,12 @@ #include "sentry_value.h" #include "transports/sentry_http_transport.h" +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_ANDROID) +# include +# include +# include +#endif + static char *const SERIALIZED_ENVELOPE_STR = "{\"dsn\":\"https://foo@sentry.invalid/42\"," "\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"trace\":{" @@ -1207,3 +1213,71 @@ SENTRY_TEST(envelope_can_add_client_report) sentry__rate_limiter_free(rl); sentry_close(); } + +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_ANDROID) +static void +sigxfsz_noop(int sig) +{ + (void)sig; +} +#endif + +SENTRY_TEST(write_envelope_partial_write_fails) +{ +#if !defined(SENTRY_PLATFORM_UNIX) || defined(SENTRY_PLATFORM_ANDROID) + SKIP_TEST(); +#else + sentry_options_t *options = sentry_options_new(); + TEST_CHECK(options != NULL); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_backend(options, NULL); + sentry_init(options); + + sentry_envelope_t *envelope = sentry__envelope_new(); + TEST_CHECK(envelope != NULL); + + size_t big_len = 40000; + char *big_buf = sentry_malloc(big_len); + TEST_CHECK(big_buf != NULL); + memset(big_buf, 'X', big_len); + sentry__envelope_add_from_buffer(envelope, big_buf, big_len, "attachment"); + sentry_free(big_buf); + + const char *test_file_str + = SENTRY_TEST_PATH_PREFIX "sentry_test_partial_write"; + + // fork() isolates the RLIMIT_FSIZE setting from the test suite + pid_t pid = fork(); + TEST_CHECK(pid >= 0); + + if (pid == 0) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigxfsz_noop; + sigaction(SIGXFSZ, &sa, NULL); + + struct rlimit rl; + rl.rlim_cur = 512; + rl.rlim_max = 512; + if (setrlimit(RLIMIT_FSIZE, &rl) != 0) { + _exit(2); + } + + int rv = sentry_envelope_write_to_file(envelope, test_file_str); + _exit(rv != 0 ? 0 : 1); + } + + int status = 0; + waitpid(pid, &status, 0); + TEST_CHECK(WIFEXITED(status)); + int child_exit = WEXITSTATUS(status); + TEST_CHECK_INT_EQUAL(child_exit, 0); + + sentry_path_t *test_path = sentry__path_from_str(test_file_str); + sentry__path_remove(test_path); + sentry__path_free(test_path); + + sentry_envelope_free(envelope); + sentry_close(); +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index ea810758b8..99d26a198a 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -406,5 +406,6 @@ XX(value_uint64) XX(value_unicode) XX(value_user) XX(value_wrong_type) +XX(write_envelope_partial_write_fails) XX(write_raw_envelope_to_file) XX(xmm_save_area_size) From c64257c9da3709fa20da4397e36f18d75d75d08c Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:58:47 +0200 Subject: [PATCH 2/6] fix(envelope): detect partial disk writes in envelope_write_to_path The streaming envelope writer (introduced in #1021) did not check individual write results, so a truncated write (e.g. ENOSPC) was reported as success as long as any bytes were written. The jsonwriter's failed flag was also cleared by reset() between items, masking earlier failures. Check every filewriter_write and jsonwriter write result, early-exit on failure, and log a single warning. Also adds a sentry__jsonwriter_has_failed() accessor so envelope code does not need to reach into the opaque jsonwriter struct. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_envelope.c | 30 ++++++++++++++++++++++++------ src/sentry_json.c | 6 ++++++ src/sentry_json.h | 5 +++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 6a15604541..66e303fa89 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -930,9 +930,13 @@ envelope_write_to_path(const sentry_envelope_t *envelope, return rv != 0; } + int failed = 1; sentry_jsonwriter_t *jw = sentry__jsonwriter_new_fw(fw); if (jw) { sentry__jsonwriter_write_value(jw, envelope->contents.items.headers); + if (sentry__jsonwriter_has_failed(jw)) { + goto done; + } sentry__jsonwriter_reset(jw); for (const sentry_envelope_item_t *item @@ -942,22 +946,36 @@ envelope_write_to_path(const sentry_envelope_t *envelope, continue; } const char newline = '\n'; - sentry__filewriter_write(fw, &newline, sizeof(char)); + if (sentry__filewriter_write(fw, &newline, sizeof(char)) != 0) { + goto done; + } sentry__jsonwriter_write_value(jw, item->headers); + if (sentry__jsonwriter_has_failed(jw)) { + goto done; + } sentry__jsonwriter_reset(jw); - sentry__filewriter_write(fw, &newline, sizeof(char)); + if (sentry__filewriter_write(fw, &newline, sizeof(char)) != 0) { + goto done; + } - sentry__filewriter_write(fw, item->payload, item->payload_len); + if (sentry__filewriter_write(fw, item->payload, item->payload_len) + != 0) { + goto done; + } } - sentry__jsonwriter_free(jw); + failed = sentry__filewriter_byte_count(fw) == 0; } - size_t rv = sentry__filewriter_byte_count(fw); +done: + if (failed) { + SENTRY_WARN("envelope write failed: partial disk write"); + } + sentry__jsonwriter_free(jw); sentry__filewriter_free(fw); - return rv == 0; + return failed; } MUST_USE int diff --git a/src/sentry_json.c b/src/sentry_json.c index b68d1a9ebd..7964ed01a9 100644 --- a/src/sentry_json.c +++ b/src/sentry_json.c @@ -208,6 +208,12 @@ sentry__jsonwriter_reset(sentry_jsonwriter_t *jw) jw->failed = false; } +bool +sentry__jsonwriter_has_failed(const sentry_jsonwriter_t *jw) +{ + return jw && jw->failed; +} + char * sentry__jsonwriter_into_string(sentry_jsonwriter_t *jw, size_t *len_out) { diff --git a/src/sentry_json.h b/src/sentry_json.h index 20f3025188..5aca6ed2e1 100644 --- a/src/sentry_json.h +++ b/src/sentry_json.h @@ -32,6 +32,11 @@ void sentry__jsonwriter_free(sentry_jsonwriter_t *jw); */ void sentry__jsonwriter_reset(sentry_jsonwriter_t *jw); +/** + * Returns true if any write operation on this JSON writer has failed. + */ +bool sentry__jsonwriter_has_failed(const sentry_jsonwriter_t *jw); + /** * This will consume and deallocate the JSON writer, returning the generated * JSON string, and writing its length into `len_out`. From 399bee751a002a9a6e82a5694ee8eac12109f983 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:02:01 +0200 Subject: [PATCH 3/6] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5cd5267b2..210cf4c4c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Fixes**: + +- Report on partial disk writes when streaming envelopes to file, which previously left truncated envelopes on disk and reported success. ([#1804](https://github.com/getsentry/sentry-native/pull/1804)) + ## 0.15.0 **Breaking**: From 972089372876abcdc20f734a0f90c45f7adbd6e2 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:04:49 +0200 Subject: [PATCH 4/6] exclude test on consoles --- tests/unit/test_envelopes.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 27daf5274c..eb80fd1a86 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -12,7 +12,8 @@ #include "sentry_value.h" #include "transports/sentry_http_transport.h" -#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_ANDROID) \ + && !defined(SENTRY_PLATFORM_PS) && !defined(SENTRY_PLATFORM_NX) # include # include # include @@ -1214,7 +1215,8 @@ SENTRY_TEST(envelope_can_add_client_report) sentry_close(); } -#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_ANDROID) \ + && !defined(SENTRY_PLATFORM_PS) && !defined(SENTRY_PLATFORM_NX) static void sigxfsz_noop(int sig) { @@ -1224,7 +1226,8 @@ sigxfsz_noop(int sig) SENTRY_TEST(write_envelope_partial_write_fails) { -#if !defined(SENTRY_PLATFORM_UNIX) || defined(SENTRY_PLATFORM_ANDROID) +#if !defined(SENTRY_PLATFORM_UNIX) || defined(SENTRY_PLATFORM_ANDROID) \ + || defined(SENTRY_PLATFORM_PS) || defined(SENTRY_PLATFORM_NX) SKIP_TEST(); #else sentry_options_t *options = sentry_options_new(); From 85ba1a20c27867ba81e64f71d7f88f4afd31e064 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:19:13 +0200 Subject: [PATCH 5/6] free envelope --- tests/unit/test_envelopes.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index eb80fd1a86..726b01ab86 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -1267,6 +1267,7 @@ SENTRY_TEST(write_envelope_partial_write_fails) } int rv = sentry_envelope_write_to_file(envelope, test_file_str); + sentry_envelope_free(envelope); _exit(rv != 0 ? 0 : 1); } From 9af7cc69903289ee0d881464eb733d0f15cf02a7 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:50:09 +0200 Subject: [PATCH 6/6] minor cleanup --- src/sentry_envelope.c | 2 +- src/sentry_json.c | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 66e303fa89..d16df6141c 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -970,7 +970,7 @@ envelope_write_to_path(const sentry_envelope_t *envelope, done: if (failed) { - SENTRY_WARN("envelope write failed: partial disk write"); + SENTRY_WARN("envelope write failed"); } sentry__jsonwriter_free(jw); sentry__filewriter_free(fw); diff --git a/src/sentry_json.c b/src/sentry_json.c index 7964ed01a9..079be8ad6a 100644 --- a/src/sentry_json.c +++ b/src/sentry_json.c @@ -196,7 +196,9 @@ sentry__jsonwriter_new_fw(sentry_filewriter_t *fw) void sentry__jsonwriter_free(sentry_jsonwriter_t *jw) { - jw->ops->free(jw); + if (jw) { + jw->ops->free(jw); + } } void