From 092e3e3cdc323b27baabe5f5c43ae7f00c8550c5 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 6 Oct 2026 00:29:28 +0200 Subject: [PATCH] Validate quota scan failures and deny unauthorized quota queries --- src/member_files/storage.c | 11 +++++++++-- tests/test_member_files.c | 19 ++++++++++++++++++- tests/test_member_files_network.c | 3 +++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/member_files/storage.c b/src/member_files/storage.c index 958fa6c7..1180fe03 100644 --- a/src/member_files/storage.c +++ b/src/member_files/storage.c @@ -365,9 +365,16 @@ static int store_command(struct mf_storage* s, uint64_t group, uint64_t peer, co struct mf_disk_info info = {0}; int rc, fd; if (q->command == MF_QUOTA) { - if (*q->path || *q->target || q->cursor || q->data_len) return MF_BAD_PACKET; + if (*q->path || *q->target || q->cursor || q->data_len) { + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: invalid quota request owner=%llu path=%s target=%s cursor=%llu data=%u", + (unsigned long long)q->owner, q->path, q->target, (unsigned long long)q->cursor, q->data_len); + return MF_BAD_PACKET; + } struct store_user* user = store_user_get(s, q->owner, 0); - if (user && !user->valid) return MF_IO_ERROR; + if (user && !user->valid) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "member_files: quota account invalid owner=%llu; restart scan required", (unsigned long long)q->owner); + return MF_IO_ERROR; + } mf_put64(r->data, user ? user->used : 0); mf_put64(r->data + 8, s->quota); mf_put64(r->data + 16, user ? user->reserved : 0); r->data_len = MF_QUOTA_BYTES; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: quota response owner=%llu used=%llu reserved=%llu limit=%llu", diff --git a/tests/test_member_files.c b/tests/test_member_files.c index 22aae627..4b066360 100644 --- a/tests/test_member_files.c +++ b/tests/test_member_files.c @@ -240,6 +240,23 @@ static void quota_test(const char* directory) { check_quota(s, 10, 202, 30, 0, 10); mf_storage_destroy(s); } +static void quota_scan_failure_test(const char* directory) { + char root[1200], rogue[1400]; snprintf(root, sizeof(root), "%s/quota-invalid", directory); + struct mf_storage* s = mf_storage_create(root, 40, 200000); + EXPECT(s && !mf_storage_initialize(s), "scan failure fixture startup"); if (!s) return; + quota_begin(s, 10, 101, "persist", 12, 51, NULL, MF_OK); quota_commit(s, 10); mf_storage_destroy(s); + snprintf(rogue, sizeof(rogue), "%s/rogue.bin", root); + FILE* file = fopen(rogue, "wb"); EXPECT(file != NULL, "invalid namespace fixture"); if (file) fclose(file); + s = mf_storage_create(root, 40, 200000); EXPECT(s != NULL, "scan failure storage allocation"); if (!s) return; + EXPECT(mf_storage_initialize(s) == MF_INVALID_PATH, "invalid namespace rejects partial startup count"); + memset(&q, 0, sizeof(q)); q.owner = 101; + run(s, 10, 101, MF_QUOTA, MF_INVALID_PATH); EXPECT(!r.data_len, "failed scan does not expose partial quota"); + mf_storage_destroy(s); test_unlink(rogue); + s = mf_storage_create(root, 40, 200000); + EXPECT(s && !mf_storage_initialize(s), "restart after namespace repair"); + if (s) { check_quota(s, 20, 101, 12, 0, 40); mf_storage_destroy(s); } +} + static void worker_test(void) { struct worker_test t = {0}; t.ua = uasync_create(); t.caller = pthread_self(); t.worker = mf_worker_create(t.ua); EXPECT(t.worker != NULL, "worker start"); @@ -256,7 +273,7 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_DEBUG); char directory[1024] = "/tmp/utun_member_files_XXXXXX"; if (test_mkdtemp(directory)) return 1; - protocol_test(); storage_test(directory); quota_test(directory); worker_test(); + protocol_test(); storage_test(directory); quota_test(directory); quota_scan_failure_test(directory); worker_test(); #ifndef _WIN32 large_offset_test(directory); #endif diff --git a/tests/test_member_files_network.c b/tests/test_member_files_network.c index 0c1c5753..51657a76 100644 --- a/tests/test_member_files_network.c +++ b/tests/test_member_files_network.c @@ -282,6 +282,9 @@ static void tick(void* arg) { t->phase = 7; request(t, MF_STAT, t->ids[AUTHOR]); } else if (t->phase == 7 && t->answered) { if (t->response.status != MF_ACCESS_DENIED) { fail(t, "deleted reader accepted"); return; } + t->phase = 21; request(t, MF_QUOTA, t->ids[AUTHOR]); + } else if (t->phase == 21 && t->answered) { + if (t->response.status != MF_ACCESS_DENIED || t->response.data_len) { fail(t, "quota leaked to deleted reader"); return; } DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files network: PASS progress=%u ticks=%u peak_inflight=%u pauses=%d", t->progress, t->ticks, t->peak_inflight, t->receive_pauses); uasync_stop(t->ua); return;