From e195894048249c2ea83fdafa2607a54d7113cc4e Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 00:13:18 +0200 Subject: [PATCH] Protect owned temporary files from concurrent orphan removal --- lib/file_io.c | 83 +++++++++++++++++++++++++++++++++++++++++++++------ lib/file_io.h | 7 +++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/lib/file_io.c b/lib/file_io.c index 17cf3926..01365190 100644 --- a/lib/file_io.c +++ b/lib/file_io.c @@ -6,6 +6,28 @@ #include #include +#ifdef _WIN32 +static SRWLOCK temp_mutex = SRWLOCK_INIT; +#define TEMP_LOCK() AcquireSRWLockExclusive(&temp_mutex) +#define TEMP_UNLOCK() ReleaseSRWLockExclusive(&temp_mutex) +#else +#include +static pthread_mutex_t temp_mutex = PTHREAD_MUTEX_INITIALIZER; +#define TEMP_LOCK() pthread_mutex_lock(&temp_mutex) +#define TEMP_UNLOCK() pthread_mutex_unlock(&temp_mutex) +#endif + +/* Реестр заимствует стабильный адрес владельца, не выделяет память и общий для workers/instances. */ +static struct file_io_temp *owned_temps; + +static void file_io_temp_untrack(struct file_io_temp *output) { + struct file_io_temp **p = &owned_temps; + while (*p && *p != output) p = &(*p)->next_owned; + if (*p) *p = output->next_owned; + else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary owner missing from registry path=%s", output->temporary); + output->next_owned = NULL; +} + #ifdef _WIN32 /* Общие пути API — UTF-8; ANSI Win32 API не применяется. */ int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) { @@ -122,19 +144,25 @@ int file_io_temp_open(struct file_io_temp *output, const char *destination) { uint8_t random[16]; char suffix[33]; if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; } for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]); - snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix); + char temporary[FILE_IO_PATH_MAX]; + snprintf(temporary, sizeof(temporary), "%s.tmp.%s", destination, suffix); #ifdef _WIN32 wchar_t wide[FILE_IO_PATH_MAX]; - if (file_io_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; } + if (file_io_wide(temporary, wide)) return -1; + TEMP_LOCK(); int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE); #else - int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600); + TEMP_LOCK(); + int fd = open(temporary, O_CREAT | O_EXCL | O_RDWR, 0600); #endif if (fd < 0) { - output->temporary[0] = 0; - if (errno == EEXIST) continue; - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, errno); return -1; + int error = errno; TEMP_UNLOCK(); + if (error == EEXIST) continue; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, error); return -1; } + memcpy(output->temporary, temporary, strlen(temporary) + 1); + output->next_owned = owned_temps; owned_temps = output; + TEMP_UNLOCK(); /* Файл виден очистке только вместе с зарегистрированным владельцем. */ #ifdef _WIN32 output->file = _fdopen(fd, "wb+"); if (!output->file) _close(fd); @@ -167,12 +195,17 @@ void file_io_temp_discard(struct file_io_temp *output) { if (!output->temporary[0]) return; #ifdef _WIN32 wchar_t wide[FILE_IO_PATH_MAX]; - int error = file_io_wide(output->temporary, wide) ? -1 : _wremove(wide); + int valid = file_io_wide(output->temporary, wide) == 0; + TEMP_LOCK(); + int error = valid ? _wremove(wide) : -1; #else + TEMP_LOCK(); int error = remove(output->temporary); #endif if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno); + file_io_temp_untrack(output); output->temporary[0] = 0; + TEMP_UNLOCK(); } int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) { @@ -182,15 +215,47 @@ int file_io_temp_publish(struct file_io_temp *output, const char *source, const #ifdef _WIN32 wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX]; if (file_io_wide(output->temporary, from) || file_io_wide(destination, to)) return -1; + TEMP_LOCK(); if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1; + DWORD error = GetLastError(); TEMP_UNLOCK(); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, error); return -1; } #else + TEMP_LOCK(); if (rename(output->temporary, destination)) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, errno); return -1; + int error = errno; TEMP_UNLOCK(); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, error); return -1; } #endif + file_io_temp_untrack(output); output->temporary[0] = 0; + TEMP_UNLOCK(); DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination); return 0; } + +int file_io_remove_orphan(const char *path) { + if (!path || !*path) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file orphan removal without path"); return -1; } +#ifdef _WIN32 + wchar_t wide[FILE_IO_PATH_MAX]; + if (file_io_wide(path, wide)) return -1; +#endif + TEMP_LOCK(); + for (struct file_io_temp *owner = owned_temps; owner; owner = owner->next_owned) { + if (!strcmp(owner->temporary, path)) { + TEMP_UNLOCK(); + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file orphan cleanup keeps active temporary path=%s", path); + return 0; + } + } +#ifdef _WIN32 + int result = _wremove(wide); +#else + int result = remove(path); +#endif + int error = errno; TEMP_UNLOCK(); + if (!result) return 1; + if (error == ENOENT) return 0; + DEBUG_WARN(DEBUG_CATEGORY_SYS, "file orphan removal failed path=%s errno=%d", path, error); + return -1; +} diff --git a/lib/file_io.h b/lib/file_io.h index de8df4a2..0964a5c8 100644 --- a/lib/file_io.h +++ b/lib/file_io.h @@ -1,6 +1,8 @@ /* file_io — синхронные файловые операции: UTF-8 пути, идентичность и публикация через соседний temp. * inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла. * Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке. + * Активный temp автоматически защищён от remove_orphan во всех потоках до publish/discard, включая время после finish. + * Пока temp активен, структуру нельзя копировать/перемещать; с одним temp работает один владелец. * Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов. * Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */ #ifndef FILE_IO_H @@ -19,6 +21,7 @@ struct file_io_info { struct file_io_temp { char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish FILE *file; // открытый wb+; NULL после finish + struct file_io_temp *next_owned; // внутренний реестр живых temp; изменяет только file_io под блокировкой }; // path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе. @@ -41,4 +44,8 @@ int file_io_temp_finish(struct file_io_temp *output); void file_io_temp_discard(struct file_io_temp *output); // Повторно проверить paths и заменить destination закрытым temp; одинаковые файлы отклоняются. int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination); +/* Удалить уже признанный сиротой файл, атомарно проверив владение file_io_temp во всех потоках. + * 1 — удалён, 0 — активный temp или уже отсутствует, -1 — ошибка (логируется). + * path должен иметь то же нормализованное написание, что и путь при temp_open; рабочий каталог не меняется. */ +int file_io_remove_orphan(const char *path); #endif