From d2dee7b8ffb7aa672cd45f67dd25c2aee5828f71 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 26 Jul 2026 09:45:54 +0200 Subject: [PATCH] diag(install): report the OS error from a failed staging copy The copy loop's failure was reported as 'os 0' because the cleanup and close calls after the loop clobber the thread's last error. Captures it at the failure point, and separates a failed read from a failed write -- copying a payload that another process still holds is a different problem from a full disk. Signed-off-by: Martin Vogel --- src/cli/activation_transaction.c | 36 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/cli/activation_transaction.c b/src/cli/activation_transaction.c index 9198493b5..b42e72e60 100644 --- a/src/cli/activation_transaction.c +++ b/src/cli/activation_transaction.c @@ -1510,10 +1510,21 @@ cbm_activation_transaction_status_t cbm_activation_transaction_stage_file( unsigned char buffer[64U * 1024U]; size_t total = 0; bool copied = true; + /* Captured AT the failure, not after: the cleanup and close calls below all + * clobber the thread's last error, which is why this path could only ever + * report "os 0". */ + unsigned long copy_os_error = 0UL; + bool copy_failed_on_read = false; for (;;) { size_t amount = 0; if (!activation_native_read(source, buffer, sizeof(buffer), &amount)) { copied = false; + copy_failed_on_read = true; +#ifdef _WIN32 + copy_os_error = (unsigned long)GetLastError(); +#else + copy_os_error = (unsigned long)errno; +#endif break; } if (amount == 0) { @@ -1521,6 +1532,11 @@ cbm_activation_transaction_status_t cbm_activation_transaction_stage_file( } if (SIZE_MAX - total < amount || !activation_native_write_all(staged, buffer, amount)) { copied = false; +#ifdef _WIN32 + copy_os_error = (unsigned long)GetLastError(); +#else + copy_os_error = (unsigned long)errno; +#endif break; } total += amount; @@ -1536,13 +1552,19 @@ cbm_activation_transaction_status_t cbm_activation_transaction_stage_file( * sync are not the same bug. Naming them is what turns a ten-minute * guard run per hypothesis into one run that answers the question. */ activation_failed_stage_cleanup(transaction); - activation_note_refusal(!copied ? "stage-copy-read-or-write" - : total == 0 ? "stage-source-empty" - : !durable ? "stage-fsync" - : !source_closed ? "stage-source-close" - : !staged_closed ? "stage-staged-close" - : "stage-directory-sync", - 0UL); + const char *why = "stage-directory-sync"; + if (!copied) { + why = copy_failed_on_read ? "stage-copy-read" : "stage-copy-write"; + } else if (total == 0) { + why = "stage-source-empty"; + } else if (!durable) { + why = "stage-fsync"; + } else if (!source_closed) { + why = "stage-source-close"; + } else if (!staged_closed) { + why = "stage-staged-close"; + } + activation_note_refusal(why, copy_os_error); return CBM_ACTIVATION_TRANSACTION_IO; } *transaction_out = transaction;