From e2a9f655e3eb6ca22f4a36d521fb5b3b8978bfad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 11:15:33 +0000 Subject: [PATCH] fix(gateway): persist pairing tokens with atomic replace auth_pair truncated auth_tokens.json in place and ignored write errors, so ENOSPC, a crash after O_TRUNC, or fdopen failure could wipe every device token and still return success with a token that never landed on disk. Write to a sibling temp file, fsync, then rename. Keep the pairing code until the store is committed so a failed pair can be retried. Co-authored-by: esadrianno --- src/gateway/auth.c | 69 ++++++++++++++++++++++++++++++++++++++-------- tests/test_auth.c | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 11 deletions(-) diff --git a/src/gateway/auth.c b/src/gateway/auth.c index 578066e..84b4f2d 100644 --- a/src/gateway/auth.c +++ b/src/gateway/auth.c @@ -166,6 +166,62 @@ static int ensure_tokens_dir(const char *path) return 0; } +static void discard_tokens_tmp(int fd, char *tmp_path) +{ + if (fd >= 0) + (void)close(fd); + if (tmp_path) { + unlink(tmp_path); + free(tmp_path); + } +} + +/* + * Replace tokens via temp+rename so O_TRUNC cannot wipe auth_tokens.json + * before the new JSON is fully on disk (ENOSPC / crash / fdopen failure). + */ +static int write_tokens_atomic(const char *path, const char *json) +{ + size_t json_len; + size_t off; + char *tmp_path; + int fd; + + if (!path || !json) return -1; + json_len = strlen(json); + tmp_path = malloc(strlen(path) + 8); + if (!tmp_path) return -1; + snprintf(tmp_path, strlen(path) + 8, "%s.tmp", path); + fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) { + free(tmp_path); + return -1; + } + off = 0; + while (off < json_len) { + ssize_t n = write(fd, json + off, json_len - off); + if (n <= 0) { + discard_tokens_tmp(fd, tmp_path); + return -1; + } + off += (size_t)n; + } + if (fsync(fd) != 0) { + discard_tokens_tmp(fd, tmp_path); + return -1; + } + if (close(fd) != 0) { + discard_tokens_tmp(-1, tmp_path); + return -1; + } + if (rename(tmp_path, path) != 0) { + discard_tokens_tmp(-1, tmp_path); + return -1; + } + free(tmp_path); + return 0; +} + int auth_pair(auth_ctx_t *ctx, const char *code, char *token_out, size_t token_size) { if (!ctx || !ctx->tokens_path || !code || !token_out || token_size == 0) return -1; @@ -174,7 +230,7 @@ int auth_pair(auth_ctx_t *ctx, const char *code, char *token_out, size_t token_s !constant_time_cmp(code, ctx->pending_pairing_code, PAIRING_CODE_LEN)) return -1; char new_token[TOKEN_LEN + 1]; - generate_random_hex(new_token, TOKEN_LEN); + if (generate_random_hex(new_token, TOKEN_LEN) != 0) return -1; /* Read existing tokens and append (multi-device support). */ cJSON *arr = NULL; { @@ -204,19 +260,10 @@ int auth_pair(auth_ctx_t *ctx, const char *code, char *token_out, size_t token_s free(json); return -1; } - int fd = open(ctx->tokens_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); - if (fd < 0) { - free(json); - return -1; - } - FILE *out = fdopen(fd, "w"); - if (!out) { - close(fd); + if (write_tokens_atomic(ctx->tokens_path, json) != 0) { free(json); return -1; } - fprintf(out, "%s", json); - fclose(out); free(json); size_t copy_len = (size_t)TOKEN_LEN < token_size - 1 ? (size_t)TOKEN_LEN : token_size - 1; memcpy(token_out, new_token, copy_len); diff --git a/tests/test_auth.c b/tests/test_auth.c index 242efdb..3f84b49 100644 --- a/tests/test_auth.c +++ b/tests/test_auth.c @@ -5,9 +5,11 @@ #define _POSIX_C_SOURCE 200809L #include "gateway/auth.h" +#include #include #include #include +#include #include #define ASSERT(c) do { if (!(c)) { fprintf(stderr, "FAIL: %s:%d %s\n", __FILE__, __LINE__, #c); return 1; } } while (0) @@ -100,6 +102,65 @@ static int test_auth_validate_token(void) return 0; } +static int test_auth_pair_write_failure_preserves_existing_tokens(void) +{ + char dir_template[] = "/tmp/shellclaw_auth_atomic_XXXXXX"; + char path[512]; + char tmp_path[512]; + char token[64]; + char *dir; + char *code; + auth_ctx_t *ctx; + FILE *tokens_file; + struct rlimit old_lim; + struct rlimit new_lim; + int pair_ret; + const char *existing = "existingtokenexistingtokenexist01"; + + dir = mkdtemp(dir_template); + ASSERT(dir != NULL); + snprintf(path, sizeof(path), "%s/auth_tokens.json", dir); + snprintf(tmp_path, sizeof(tmp_path), "%s/auth_tokens.json.tmp", dir); + + ctx = auth_init(path); + ASSERT(ctx != NULL); + code = auth_get_or_create_pairing_code(ctx); + ASSERT(code != NULL); + + /* Pairing code is in memory; tokens file already has a device (append window). */ + tokens_file = fopen(path, "w"); + ASSERT(tokens_file != NULL); + ASSERT(fprintf(tokens_file, "[\"%s\"]", existing) > 0); + ASSERT(fclose(tokens_file) == 0); + ASSERT(auth_validate_token(ctx, existing) == 1); + + ASSERT(getrlimit(RLIMIT_FSIZE, &old_lim) == 0); + new_lim = old_lim; + new_lim.rlim_cur = 8; + (void)signal(SIGXFSZ, SIG_IGN); + ASSERT(setrlimit(RLIMIT_FSIZE, &new_lim) == 0); + + memset(token, 0, sizeof(token)); + pair_ret = auth_pair(ctx, code, token, sizeof(token)); + ASSERT(setrlimit(RLIMIT_FSIZE, &old_lim) == 0); + + ASSERT(pair_ret != 0); + ASSERT(token[0] == '\0'); + ASSERT(auth_validate_token(ctx, existing) == 1); + + memset(token, 0, sizeof(token)); + ASSERT(auth_pair(ctx, code, token, sizeof(token)) == 0); + ASSERT(auth_validate_token(ctx, existing) == 1); + ASSERT(auth_validate_token(ctx, token) == 1); + + free(code); + auth_cleanup(ctx); + unlink(path); + unlink(tmp_path); + ASSERT(rmdir(dir) == 0); + return 0; +} + static int test_auth_multi_token(void) { const char *path = "/tmp/shellclaw_test_tokens_multi.json"; @@ -215,6 +276,10 @@ int main(void) if (test_auth_pair_valid_code() != 0) { fprintf(stderr, "test_auth_pair_valid_code failed\n"); failed++; } if (test_auth_pair_invalid_code() != 0) { fprintf(stderr, "test_auth_pair_invalid_code failed\n"); failed++; } if (test_auth_validate_token() != 0) { fprintf(stderr, "test_auth_validate_token failed\n"); failed++; } + if (test_auth_pair_write_failure_preserves_existing_tokens() != 0) { + fprintf(stderr, "test_auth_pair_write_failure_preserves_existing_tokens failed\n"); + failed++; + } if (test_auth_multi_token() != 0) { fprintf(stderr, "test_auth_multi_token failed\n"); failed++; } if (test_pair_lockout_triggers_after_max_fails() != 0) { fprintf(stderr, "test_pair_lockout_triggers_after_max_fails failed\n"); failed++; } if (test_pair_lockout_expires() != 0) { fprintf(stderr, "test_pair_lockout_expires failed\n"); failed++; }