Protect owned temporary files from concurrent orphan removal
This commit is contained in:
1 parent
ee437b9473
commit
e195894048
2 files changed
+81
-9
No files matched your search
+74
-9
@@ -6,6 +6,28 @@
|
||||
#include <sys/stat.h>
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
static SRWLOCK temp_mutex = SRWLOCK_INIT;
|
||||
#define TEMP_LOCK() AcquireSRWLockExclusive(&temp_mutex)
|
||||
#define TEMP_UNLOCK() ReleaseSRWLockExclusive(&temp_mutex)
|
||||
#else
|
||||
#include <pthread.h>
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user