fix: prevent segfault from rapid module writes

Add platform-specific protections against race conditions when
module files are written rapidly (compilation, copying, etc).

Linux (inotify):
- Deduplicate events within a poll cycle to prevent double-unload
- When duplicate RELOAD events occur, earlier events are skipped
  and only the final event is processed

BSD (kqueue):
- Add file readiness check with flock() before processing mtime changes
- Files that aren't ready have their mtime reset, skipping the reload

All Unix platforms:
- Use atomic .tmp + rename for module copying to prevent loading
  partially-written files

Writes that occur during an active reload or when files aren't
ready are skipped. A subsequent write is required to trigger
detection of those changes.

Fixes segfault caused by processing duplicate reload events
(Linux) or copying incomplete files mid-write (BSD).
This commit is contained in:
2026-01-31 13:59:57 +01:00
parent 8362889d73
commit 3a06023265
+176 -94
View File
@@ -4,6 +4,10 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#ifndef _WIN32
#include <sys/file.h>
#endif
#if defined(__linux__) #if defined(__linux__)
#include <dirent.h> #include <dirent.h>
#include <dlfcn.h> #include <dlfcn.h>
@@ -28,10 +32,11 @@ int is_mod_loaded(const char *module_name);
uint8_t is_valid_module_file(const char *filename); uint8_t is_valid_module_file(const char *filename);
void extract_module_id(const char *path, char *out_id); void extract_module_id(const char *path, char *out_id);
#ifdef _WIN32 #ifndef __linux__
static uint8_t is_file_ready(const char *dir_path, const char *filename) static uint8_t is_file_ready(const char *dir_path, const char *filename)
{ {
char full_path[STK_PATH_MAX_OS]; char full_path[STK_PATH_MAX_OS];
#ifdef _WIN32
DWORD size; DWORD size;
HANDLE h; HANDLE h;
@@ -48,10 +53,33 @@ static uint8_t is_file_ready(const char *dir_path, const char *filename)
return 0; return 0;
return 1; return 1;
#else
int fd;
struct stat st;
sprintf(full_path, "%s/%s", dir_path, filename);
if (stat(full_path, &st) != 0)
return 0;
if (st.st_size < 1024)
return 0;
fd = open(full_path, O_RDONLY);
if (fd < 0)
return 0;
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
close(fd);
return 0;
}
flock(fd, LOCK_UN);
close(fd);
return 1;
#endif
} }
#endif
#ifndef __linux__
typedef struct { typedef struct {
char filename[STK_PATH_MAX]; char filename[STK_PATH_MAX];
#ifdef _WIN32 #ifdef _WIN32
@@ -116,26 +144,62 @@ uint8_t platform_copy_file(const char *from, const char *to)
} }
#else #else
FILE *src = NULL, *dst = NULL; FILE *src = NULL, *dst = NULL;
char tmp_path[STK_PATH_MAX_OS];
size_t n; size_t n;
#ifndef __linux__
char dir_path[STK_PATH_MAX_OS];
const char *filename;
filename = strrchr(from, '/');
if (filename) {
size_t dir_len = filename - from;
strncpy(dir_path, from, dir_len);
dir_path[dir_len] = '\0';
filename++;
} else {
strcpy(dir_path, ".");
filename = from;
}
if (!is_file_ready(dir_path, filename))
goto done;
#endif
sprintf(tmp_path, "%s.tmp", to);
src = fopen(from, "rb"); src = fopen(from, "rb");
if (!src) if (!src)
goto cleanup; goto cleanup;
dst = fopen(to, "wb"); dst = fopen(tmp_path, "wb");
if (!dst) if (!dst)
goto cleanup; goto cleanup;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0) while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
fwrite(buf, 1, n, dst); fwrite(buf, 1, n, dst);
ret = STK_PLATFORM_OPERATION_SUCCESS; if (src)
fclose(src);
if (dst)
fclose(dst);
src = NULL;
dst = NULL;
if (rename(tmp_path, to) == 0)
ret = STK_PLATFORM_OPERATION_SUCCESS;
else
unlink(tmp_path);
goto done;
cleanup: cleanup:
if (src) if (src)
fclose(src); fclose(src);
if (dst) if (dst)
fclose(dst); fclose(dst);
unlink(tmp_path);
done:
#endif #endif
return ret; return ret;
@@ -222,115 +286,116 @@ void *platform_get_symbol(void *h, const char *s)
#endif #endif
} }
char (*platform_directory_init_scan(const char *dir_path, size_t *out_count)) char (*platform_directory_init_scan(const char *dir_path,
[STK_PATH_MAX] { size_t *out_count))[STK_PATH_MAX]
size_t count = 0, i = 0; {
char (*list)[STK_PATH_MAX] = NULL; size_t count = 0, i = 0;
char(*list)[STK_PATH_MAX] = NULL;
#ifdef _WIN32 #ifdef _WIN32
WIN32_FIND_DATAA fd; WIN32_FIND_DATAA fd;
HANDLE h; HANDLE h;
char s[STK_PATH_MAX_OS]; char s[STK_PATH_MAX_OS];
sprintf(s, "%s\\*", dir_path); sprintf(s, "%s\\*", dir_path);
h = FindFirstFileA(s, &fd); h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE) if (h == INVALID_HANDLE_VALUE)
goto create_and_exit; goto create_and_exit;
do { do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue; continue;
if (is_valid_module_file(fd.cFileName)) if (is_valid_module_file(fd.cFileName))
count++; count++;
} while (FindNextFileA(h, &fd)); } while (FindNextFileA(h, &fd));
FindClose(h); FindClose(h);
if (count == 0) if (count == 0)
goto exit; goto exit;
list = malloc(count * sizeof(*list)); list = malloc(count * sizeof(*list));
if (!list) if (!list)
goto exit; goto exit;
h = FindFirstFileA(s, &fd); h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE) if (h == INVALID_HANDLE_VALUE)
goto exit; goto exit;
do { do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue; continue;
if (is_valid_module_file(fd.cFileName) && i < count) if (is_valid_module_file(fd.cFileName) && i < count)
strncpy(list[i++], fd.cFileName, STK_PATH_MAX - 1); strncpy(list[i++], fd.cFileName, STK_PATH_MAX - 1);
} while (FindNextFileA(h, &fd)); } while (FindNextFileA(h, &fd));
FindClose(h); FindClose(h);
goto exit; goto exit;
create_and_exit: create_and_exit:
platform_mkdir(dir_path); platform_mkdir(dir_path);
exit: exit:
*out_count = i; *out_count = i;
return list; return list;
#else #else
DIR *d; DIR *d;
struct dirent *e; struct dirent *e;
struct stat st; struct stat st;
char f[STK_PATH_MAX_OS]; char f[STK_PATH_MAX_OS];
d = opendir(dir_path); d = opendir(dir_path);
if (!d) if (!d)
goto create_and_exit; goto create_and_exit;
count_loop: count_loop:
e = readdir(d); e = readdir(d);
if (!e) if (!e)
goto count_done; goto count_done;
sprintf(f, "%s/%s", dir_path, e->d_name); sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name)) if (!is_valid_module_file(e->d_name))
goto count_loop; goto count_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode)) if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto count_loop; goto count_loop;
count++; count++;
goto count_loop; goto count_loop;
count_done: count_done:
if (count == 0) if (count == 0)
goto close_and_exit; goto close_and_exit;
rewinddir(d); rewinddir(d);
list = malloc(count * sizeof(*list)); list = malloc(count * sizeof(*list));
if (!list) if (!list)
goto close_and_exit; goto close_and_exit;
fill_loop: fill_loop:
e = readdir(d); e = readdir(d);
if (!e || i >= count) if (!e || i >= count)
goto close_and_exit; goto close_and_exit;
sprintf(f, "%s/%s", dir_path, e->d_name); sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name)) if (!is_valid_module_file(e->d_name))
goto fill_loop; goto fill_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode)) if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto fill_loop; goto fill_loop;
strncpy(list[i++], e->d_name, STK_PATH_MAX - 1); strncpy(list[i++], e->d_name, STK_PATH_MAX - 1);
goto fill_loop; goto fill_loop;
create_and_exit: create_and_exit:
platform_mkdir(dir_path); platform_mkdir(dir_path);
*out_count = 0; *out_count = 0;
return NULL; return NULL;
close_and_exit: close_and_exit:
closedir(d); closedir(d);
*out_count = i; *out_count = i;
return list; return list;
#endif #endif
} }
#if !defined(__linux__) && !defined(_WIN32) #if !defined(__linux__) && !defined(_WIN32)
static void update_watches(platform_watch_context_t *ctx) static void update_watches(platform_watch_context_t *ctx)
@@ -585,7 +650,7 @@ stk_module_event_t *platform_directory_watch_check(
int fd = (int)(long)handle; int fd = (int)(long)handle;
char buf[STK_EVENT_BUFFER]; char buf[STK_EVENT_BUFFER];
ssize_t len; ssize_t len;
size_t index = 0, count = 0; size_t index = 0, count = 0, i;
stk_module_event_t *evs; stk_module_event_t *evs;
char *ptr, *end; char *ptr, *end;
struct inotify_event *e; struct inotify_event *e;
@@ -660,6 +725,7 @@ stk_module_event_t *platform_directory_watch_check(
if (e->mask & (IN_CLOSE_WRITE | IN_MOVED_TO)) { if (e->mask & (IN_CLOSE_WRITE | IN_MOVED_TO)) {
char event_module_name[STK_MOD_ID_BUFFER]; char event_module_name[STK_MOD_ID_BUFFER];
extract_module_id(e->name, event_module_name); extract_module_id(e->name, event_module_name);
if (is_mod_loaded(event_module_name) >= 0) if (is_mod_loaded(event_module_name) >= 0)
event_type = STK_MOD_RELOAD; event_type = STK_MOD_RELOAD;
else else
@@ -672,6 +738,16 @@ stk_module_event_t *platform_directory_watch_check(
ptr += sizeof(struct inotify_event) + e->len; ptr += sizeof(struct inotify_event) + e->len;
} }
for (i = 0; i < index; ++i) {
size_t j;
for (j = i + 1; j < index; ++j) {
if (strcmp((*file_list)[i], (*file_list)[j]) == 0) {
evs[i] = -1;
break;
}
}
}
*out_count = index; *out_count = index;
return evs; return evs;
@@ -822,10 +898,16 @@ build_diff:
} }
#else #else
if (ctx->snaps[i].mtime != new_snaps[j].mtime) { if (ctx->snaps[i].mtime != new_snaps[j].mtime) {
strncpy((*file_list)[ev_index], if (is_file_ready(ctx->path,
new_snaps[j].filename, new_snaps[j].filename)) {
STK_PATH_MAX - 1); strncpy((*file_list)[ev_index],
evs[ev_index++] = STK_MOD_RELOAD; new_snaps[j].filename,
STK_PATH_MAX - 1);
evs[ev_index++] = STK_MOD_RELOAD;
} else {
new_snaps[j].mtime =
ctx->snaps[i].mtime;
}
} }
#endif #endif
break; break;