Skip to content

Commit 4813395

Browse files
hexbyt3claude
andcommitted
Harden CFW/firmware updates with size verification and error logging
The CFW updater could fail with a bare "Extraction failed" and no explanation. Root cause: a truncated/corrupt download passed downloadFile's checks (it only inspected curl's return + HTTP status, never the size), then minizip's unzOpen rejected the bad zip and extractZip returned a generic -1 — aborting before any file was written and leaving the SD untouched. Changes: - download: add downloadFileChecked() which verifies the saved file matches the asset's expected size (from the GitHub release JSON) and retries up to DOWNLOAD_MAX_ATTEMPTS, transparently surviving flaky transfers. Capture each asset's "size" in cfw_mgr/firmware_mgr. - extract: extractZip now returns distinct codes (EXTRACT_ERR_OPEN/INDEX/ MEMORY) so callers can say *why* it failed instead of "Extraction failed". - applog: new best-effort logger writing /config/AetherBlock/last_error.log (rotated past APP_LOG_MAX_SIZE) capturing the failing step, expected vs actual sizes, and extract codes — so a failure leaves a diagnosable trail. - error_text now includes the specific reason for both CFW and firmware flows. Bump APP_VERSION to 2.0.8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6a8abb2 commit 4813395

13 files changed

Lines changed: 217 additions & 22 deletions

File tree

.github/workflows/build-release.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ jobs:
2020

2121
- name: Install dependencies
2222
run: |
23-
dkp-pacman -Sy --noconfirm switch-curl switch-mbedtls switch-zlib \
24-
|| echo "Package sync failed — checking if already installed..." \
25-
&& dkp-pacman -Q switch-curl switch-mbedtls switch-zlib
23+
# Refresh the package DB and pull the latest libnx + portlibs so
24+
# the build always targets current firmware/Atmosphere support
25+
# rather than whatever libnx the container image was baked with.
26+
dkp-pacman -Sy --noconfirm
27+
dkp-pacman -S --noconfirm --needed libnx switch-curl switch-mbedtls switch-zlib
28+
echo "## Toolchain" >> $GITHUB_STEP_SUMMARY
29+
dkp-pacman -Q libnx switch-curl switch-mbedtls switch-zlib | tee -a $GITHUB_STEP_SUMMARY
2630
2731
- name: Build
2832
run: make

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ROMFS := romfs
1818

1919
APP_TITLE := AetherBlock
2020
APP_AUTHOR := HeXbyt3
21-
APP_VERSION := 2.0.7
21+
APP_VERSION := 2.0.8
2222

2323
#---------------------------------------------------------------------------------
2424
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE

include/applog.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#ifndef APPLOG_H
2+
#define APPLOG_H
3+
4+
/* Best-effort line logger. Appends to APP_LOG_PATH so a failed update
5+
leaves behind a human-readable reason instead of only an on-screen
6+
message. Any failure to write the log is silently ignored — logging
7+
must never affect the operation it is recording. */
8+
void appLog(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
9+
10+
/* Start a new titled section. Rotates (truncates) the log first if it
11+
has grown past APP_LOG_MAX_SIZE so it can't grow without bound. */
12+
void appLogSection(const char *title);
13+
14+
#endif

include/cfw_mgr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ typedef struct {
1818
char current_ams[32];
1919
char latest_tag[32];
2020
char download_url[512];
21+
long long download_size; /* expected asset size in bytes, 0 if unknown */
2122
char changelog[4096];
2223
bool update_available;
2324
bool is_mariko;

include/config.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#define CONFIG_H
33

44
#define APP_NAME "AetherBlock"
5-
#define APP_VERSION "2.0.7"
5+
#define APP_VERSION "2.0.8"
66
#define APP_AUTHOR "HeXbyt3"
77

88
#define HOSTS_MAX_ENTRIES 256
@@ -41,4 +41,9 @@
4141
#define AETHERBLOCK_CONFIG_DIR "/config/AetherBlock/"
4242
#define FW_CLEANUP_MARKER_PATH "/config/AetherBlock/fw_cleanup_pending"
4343

44+
/* diagnostics + download integrity */
45+
#define APP_LOG_PATH "/config/AetherBlock/last_error.log"
46+
#define APP_LOG_MAX_SIZE 16384 /* rotate the log past this many bytes */
47+
#define DOWNLOAD_MAX_ATTEMPTS 3 /* retry truncated/failed transfers */
48+
4449
#endif

include/download.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ void downloadGlobalCleanup(void);
1212
int downloadFile(const char *url, const char *output_path,
1313
DownloadProgressCb cb, void *userdata);
1414

15+
/* Like downloadFile but retries up to max_attempts and, when
16+
expected_size > 0, requires the saved file to match that size exactly
17+
(catching truncated/corrupt transfers that otherwise pass as success).
18+
On failure a human-readable reason is copied into reason_out.
19+
Returns 0 on success, -1 once all attempts are exhausted. */
20+
int downloadFileChecked(const char *url, const char *output_path,
21+
long long expected_size, int max_attempts,
22+
DownloadProgressCb cb, void *userdata,
23+
char *reason_out, size_t reason_out_size);
24+
1525
int downloadToMemory(const char *url, char **out_buf, size_t *out_len);
1626

1727
int fetchJson(const char *url, cJSON **out_json);

include/extract.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33

44
#include <stddef.h>
55

6+
/* Negative return values from extractZip — hard failures that abort the
7+
whole operation before any file is written. A non-negative return is
8+
the count of individual entries that failed mid-extraction. */
9+
enum {
10+
EXTRACT_ERR_OPEN = -1, /* zip could not be opened (corrupt/truncated) */
11+
EXTRACT_ERR_INDEX = -2, /* archive directory could not be read */
12+
EXTRACT_ERR_MEMORY = -3, /* out of memory for the copy buffer */
13+
};
14+
615
typedef void (*ExtractProgressCb)(int current, int total, const char *filename, void *userdata);
716

817
/* If stage_locked_files is non-zero, any file that can't be written

include/firmware_mgr.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
#define FW_MAX_ENTRIES 64
88

99
typedef struct {
10-
char version[32];
11-
char url[512];
10+
char version[32];
11+
char url[512];
12+
long long size; /* expected asset size in bytes, 0 if unknown */
1213
} FirmwareEntry;
1314

1415
typedef enum {

source/applog.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include "applog.h"
2+
#include "config.h"
3+
#include <stdio.h>
4+
#include <stdarg.h>
5+
#include <sys/stat.h>
6+
7+
void appLog(const char *fmt, ...) {
8+
mkdir(AETHERBLOCK_CONFIG_DIR, 0755);
9+
FILE *fp = fopen(APP_LOG_PATH, "a");
10+
if (!fp) return;
11+
12+
va_list ap;
13+
va_start(ap, fmt);
14+
vfprintf(fp, fmt, ap);
15+
va_end(ap);
16+
fputc('\n', fp);
17+
fclose(fp);
18+
}
19+
20+
void appLogSection(const char *title) {
21+
mkdir(AETHERBLOCK_CONFIG_DIR, 0755);
22+
23+
/* rotate once the log gets large so it never grows without bound */
24+
const char *mode = "a";
25+
struct stat st;
26+
if (stat(APP_LOG_PATH, &st) == 0 && st.st_size > APP_LOG_MAX_SIZE)
27+
mode = "w";
28+
29+
FILE *fp = fopen(APP_LOG_PATH, mode);
30+
if (!fp) return;
31+
fprintf(fp, "\n===== %s =====\n", title);
32+
fclose(fp);
33+
}

source/cfw_mgr.c

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "pending.h"
55
#include "cfw_detect.h"
66
#include "config.h"
7+
#include "applog.h"
78
#include <cJSON.h>
89
#include <switch.h>
910
#include <stdio.h>
@@ -60,6 +61,8 @@ static int parse_release(cJSON *json, CfwPackageManager *cm) {
6061
if (strstr(name->valuestring, variant) && strstr(name->valuestring, ".zip")) {
6162
snprintf(cm->download_url, sizeof(cm->download_url),
6263
"%s", url->valuestring);
64+
cJSON *size = cJSON_GetObjectItem(asset, "size");
65+
cm->download_size = cJSON_IsNumber(size) ? (long long)size->valuedouble : 0;
6366
return 0;
6467
}
6568
}
@@ -112,9 +115,20 @@ static void *download_worker(void *arg) {
112115

113116
mkdir(AETHERBLOCK_CONFIG_DIR, 0755);
114117

115-
if (downloadFile(cm->download_url, CFW_DOWNLOAD_PATH, progress_cb, cm) != 0) {
118+
appLogSection("CFW UPDATE");
119+
appLog("package: %s console: %s", cm->latest_tag,
120+
cm->is_mariko ? "Mariko/mod-chipped" : "Erista/unpatched");
121+
appLog("url: %s", cm->download_url);
122+
appLog("expected size: %lld bytes", cm->download_size);
123+
124+
char reason[160];
125+
if (downloadFileChecked(cm->download_url, CFW_DOWNLOAD_PATH,
126+
cm->download_size, DOWNLOAD_MAX_ATTEMPTS,
127+
progress_cb, cm, reason, sizeof(reason)) != 0) {
116128
cm->state = CFW_STATE_ERROR;
117-
snprintf(cm->error_text, sizeof(cm->error_text), "Download failed");
129+
snprintf(cm->error_text, sizeof(cm->error_text), "Download failed: %s", reason);
130+
appLog("ERROR: download failed after %d attempts: %s",
131+
DOWNLOAD_MAX_ATTEMPTS, reason);
118132
cm->worker_active = false;
119133
return NULL;
120134
}
@@ -130,8 +144,20 @@ static void *download_worker(void *arg) {
130144
cm->failed_files, sizeof(cm->failed_files),
131145
1);
132146
if (extract_errs < 0) {
147+
const char *why;
148+
switch (extract_errs) {
149+
case EXTRACT_ERR_OPEN:
150+
why = "package corrupt - delete cfw_package.zip and retry"; break;
151+
case EXTRACT_ERR_INDEX:
152+
why = "archive index unreadable (corrupt download)"; break;
153+
case EXTRACT_ERR_MEMORY:
154+
why = "out of memory"; break;
155+
default:
156+
why = "unknown error"; break;
157+
}
133158
cm->state = CFW_STATE_ERROR;
134-
snprintf(cm->error_text, sizeof(cm->error_text), "Extraction failed");
159+
snprintf(cm->error_text, sizeof(cm->error_text), "Extraction failed: %s", why);
160+
appLog("ERROR: extraction aborted (code %d): %s", extract_errs, why);
135161
cm->worker_active = false;
136162
return NULL;
137163
}
@@ -140,13 +166,17 @@ static void *download_worker(void *arg) {
140166

141167
cm->state = CFW_STATE_DONE;
142168
cm->progress = 1.0f;
143-
if (extract_errs > 0)
169+
if (extract_errs > 0) {
144170
snprintf(cm->status_text, sizeof(cm->status_text),
145171
"CFW %s installed, %d file%s failed.",
146172
cm->latest_tag, extract_errs, extract_errs == 1 ? "" : "s");
147-
else
173+
appLog("done with %d file error(s): %s",
174+
extract_errs, cm->failed_files[0] ? cm->failed_files : "(none recorded)");
175+
} else {
148176
snprintf(cm->status_text, sizeof(cm->status_text),
149177
"CFW package %s installed!", cm->latest_tag);
178+
appLog("done: CFW %s installed cleanly", cm->latest_tag);
179+
}
150180
cm->worker_active = false;
151181
return NULL;
152182
}

0 commit comments

Comments
 (0)