6 Commits

Author SHA1 Message Date
anth64 fb0d8adb8f chore: bump version to 0.0.4 2026-02-11 00:22:59 +01:00
anth64 2c4d27f915 fix(linux): prevent segfault from invalid module indices during rapid reloads
When spamming file changes rapidly, inotify can report stale UNLOAD/RELOAD
events for modules that were already unloaded by previous events in the same
poll cycle. This caused is_mod_loaded() to return -1, which was then cast to
size_t (18446744073709551615) and used as an array index, causing segfaults.

Additionally, event counts were calculated before validation, causing loops to
run more iterations than valid indices were populated, reading garbage values.

Changes:
- stk.c: Check if is_mod_loaded() returns valid index (>= 0) before adding
  to unload/reload lists
- stk.c: Reset and recalculate counts after populating arrays with only valid
  indices to prevent loop overrun
- Skip processing events for modules that are no longer loaded

This completes the Linux stability fixes started in v0.0.2.
2026-02-11 00:17:33 +01:00
anth64 96fb957991 chore: bump version to 0.0.3 2026-02-09 23:03:35 +01:00
anth64 0cbee45ad2 fix(platform): replace strncpy with memmove to silence -Wrestrict warning 2026-02-09 22:53:42 +01:00
anth64 142a61a843 chore: bump version to 0.0.2 2026-02-09 22:32:55 +01:00
anth64 f83f2051ef fix(linux): prevent segfaults during rapid module reloads
Fixes multiple issues causing segfaults when hot-reloading modules
on Linux, particularly when file changes are detected rapidly:

- Enable is_file_ready() check on Linux to prevent loading partially-written
  shared libraries (previously only used on Windows/BSD)
- Fix event deduplication on Linux to actually remove duplicate inotify events
  instead of just marking them, preventing double-free on same module
- Reorder reload operations to unload old module only after successfully
  copying new version, avoiding invalid state when copy fails

Changes:
- platform.c: Remove __linux__ guards around is_file_ready() function
- platform.c: Add compaction step after deduplication to remove -1 entries
- stk.c: Move module unload to after platform_copy_file() in reload loop

These changes make Linux hot-reload as robust as Windows/BSD implementations.
2026-02-09 22:25:02 +01:00
5 changed files with 165 additions and 115 deletions
+31 -1
View File
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.0.4] - 2026-02-11
### Fixed
- **Linux**: Fixed segfault from invalid module indices during extremely rapid file changes
- Added validation check to skip stale UNLOAD/RELOAD events for already-unloaded modules
- Prevents is_mod_loaded() returning -1 from being used as array index (SIZE_MAX)
- Fixed event count mismatch where loops would run more iterations than valid indices populated
- Completes the Linux hot-reload stability fixes from v0.0.2
## [0.0.3] - 2026-02-10
### Fixed
- **Compilation**: Fixed GCC `-Wrestrict` warning in Linux directory watching code by replacing `strncpy` with `memmove` for overlapping memory operations
- Ensures clean compilation without warnings while maintaining identical runtime behavior
- Uses semantically correct function for moving memory within the same buffer
## [0.0.2] - 2026-02-09
### Fixed
- **Linux**: Fixed segfaults during rapid module reloads when file changes are detected in quick succession
- Enabled file readiness checks on Linux (previously only used on Windows/BSD) to prevent loading partially-written shared libraries
- Fixed inotify event deduplication to actually remove duplicate events instead of just marking them
- Reordered reload operations to only unload old module after successfully copying new version, preventing invalid state when copy fails
- **All platforms**: Improved reload safety by deferring module unload until after successful file copy
### Changed
- Made `is_file_ready()` check available on all Unix platforms (was previously excluded on Linux)
## [0.0.1] - 2026-02-01
### Added
@@ -25,5 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dependency management and versioning not yet implemented
- API is unstable and subject to change in future releases
[Unreleased]: https://github.com/anth64/stk/compare/v0.0.1...HEAD
[Unreleased]: https://github.com/anth64/stk/compare/v0.0.3...HEAD
[0.0.3]: https://github.com/anth64/stk/releases/tag/v0.0.3
[0.0.2]: https://github.com/anth64/stk/releases/tag/v0.0.2
[0.0.1]: https://github.com/anth64/stk/releases/tag/v0.0.1
+3 -2
View File
@@ -173,14 +173,15 @@ stk_init();
## Project Status
**Current Version:** 0.0.1 (Pre-release)
**Current Version:** 0.0.4 (Pre-release)
This is an early release proving the core hot-reload foundation. Phase 1 is still in progress.
This is a bugfix release completing the Linux hot-reload stability improvements. Phase 1 is still in progress.
### What Works
- Cross-platform module loading and hot-reloading
- File watching (inotify/kqueue/FindFirstFile)
- Basic error handling
- Robust hot-reload even during extremely rapid file changes (Linux fixes in 0.0.2-0.0.4)
### In Progress (Phase 1)
- Complete logging system (log levels, verbosity, output configuration)
+1 -1
View File
@@ -3,7 +3,7 @@
#define STK_VERSION_MAJOR 0
#define STK_VERSION_MINOR 0
#define STK_VERSION_PATCH 1
#define STK_VERSION_PATCH 4
#define STK_STRINGIFY_HELPER(x) #x
#define STK_STRINGIFY(x) STK_STRINGIFY_HELPER(x)
+110 -100
View File
@@ -32,7 +32,6 @@ int is_mod_loaded(const char *module_name);
uint8_t is_valid_module_file(const char *filename);
void extract_module_id(const char *path, char *out_id);
#ifndef __linux__
static uint8_t is_file_ready(const char *dir_path, const char *filename)
{
char full_path[STK_PATH_MAX_OS];
@@ -80,6 +79,7 @@ static uint8_t is_file_ready(const char *dir_path, const char *filename)
#endif
}
#ifndef __linux__
typedef struct {
char filename[STK_PATH_MAX];
#ifdef _WIN32
@@ -146,7 +146,6 @@ uint8_t platform_copy_file(const char *from, const char *to)
FILE *src = NULL, *dst = NULL;
char tmp_path[STK_PATH_MAX_OS];
size_t n;
#ifndef __linux__
char dir_path[STK_PATH_MAX_OS];
const char *filename;
@@ -163,7 +162,6 @@ uint8_t platform_copy_file(const char *from, const char *to)
if (!is_file_ready(dir_path, filename))
goto done;
#endif
sprintf(tmp_path, "%s.tmp", to);
@@ -286,128 +284,127 @@ void *platform_get_symbol(void *h, const char *s)
#endif
}
char (*platform_directory_init_scan(const char *dir_path,
size_t *out_count))[STK_PATH_MAX]
{
size_t count = 0, i = 0, name_len;
char(*list)[STK_PATH_MAX] = NULL;
char (*platform_directory_init_scan(const char *dir_path, size_t *out_count))
[STK_PATH_MAX] {
size_t count = 0, i = 0, name_len;
char (*list)[STK_PATH_MAX] = NULL;
#ifdef _WIN32
WIN32_FIND_DATAA fd;
HANDLE h;
char s[STK_PATH_MAX_OS];
WIN32_FIND_DATAA fd;
HANDLE h;
char s[STK_PATH_MAX_OS];
sprintf(s, "%s\\*", dir_path);
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto create_and_exit;
sprintf(s, "%s\\*", dir_path);
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto create_and_exit;
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName))
count++;
} while (FindNextFileA(h, &fd));
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName))
count++;
} while (FindNextFileA(h, &fd));
FindClose(h);
FindClose(h);
if (count == 0)
goto exit;
if (count == 0)
goto exit;
list = malloc(count * sizeof(*list));
if (!list)
goto exit;
list = malloc(count * sizeof(*list));
if (!list)
goto exit;
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto exit;
h = FindFirstFileA(s, &fd);
if (h == INVALID_HANDLE_VALUE)
goto exit;
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName) && i < count) {
name_len = strlen(fd.cFileName);
if (name_len >= STK_PATH_MAX)
name_len = STK_PATH_MAX - 1;
do {
if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
continue;
if (is_valid_module_file(fd.cFileName) && i < count) {
name_len = strlen(fd.cFileName);
if (name_len >= STK_PATH_MAX)
name_len = STK_PATH_MAX - 1;
memcpy(list[i], fd.cFileName, name_len);
list[i][name_len] = '\0';
i++;
}
} while (FindNextFileA(h, &fd));
memcpy(list[i], fd.cFileName, name_len);
list[i][name_len] = '\0';
i++;
}
} while (FindNextFileA(h, &fd));
FindClose(h);
goto exit;
FindClose(h);
goto exit;
create_and_exit:
platform_mkdir(dir_path);
exit:
*out_count = i;
return list;
create_and_exit:
platform_mkdir(dir_path);
exit:
*out_count = i;
return list;
#else
DIR *d;
struct dirent *e;
struct stat st;
char f[STK_PATH_MAX_OS];
DIR *d;
struct dirent *e;
struct stat st;
char f[STK_PATH_MAX_OS];
d = opendir(dir_path);
if (!d)
goto create_and_exit;
d = opendir(dir_path);
if (!d)
goto create_and_exit;
count_loop:
e = readdir(d);
if (!e)
goto count_done;
count_loop:
e = readdir(d);
if (!e)
goto count_done;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto count_loop;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto count_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto count_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto count_loop;
count++;
goto count_loop;
count++;
goto count_loop;
count_done:
if (count == 0)
goto close_and_exit;
count_done:
if (count == 0)
goto close_and_exit;
rewinddir(d);
list = malloc(count * sizeof(*list));
if (!list)
goto close_and_exit;
rewinddir(d);
list = malloc(count * sizeof(*list));
if (!list)
goto close_and_exit;
fill_loop:
e = readdir(d);
if (!e || i >= count)
goto close_and_exit;
fill_loop:
e = readdir(d);
if (!e || i >= count)
goto close_and_exit;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto fill_loop;
sprintf(f, "%s/%s", dir_path, e->d_name);
if (!is_valid_module_file(e->d_name))
goto fill_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto fill_loop;
if (stat(f, &st) != 0 || !S_ISREG(st.st_mode))
goto fill_loop;
name_len = strlen(e->d_name);
if (name_len >= STK_PATH_MAX) {
name_len = STK_PATH_MAX - 1;
}
memcpy(list[i++], e->d_name, name_len);
list[i - 1][name_len] = '\0';
goto fill_loop;
name_len = strlen(e->d_name);
if (name_len >= STK_PATH_MAX) {
name_len = STK_PATH_MAX - 1;
}
memcpy(list[i++], e->d_name, name_len);
list[i - 1][name_len] = '\0';
goto fill_loop;
create_and_exit:
platform_mkdir(dir_path);
*out_count = 0;
return NULL;
create_and_exit:
platform_mkdir(dir_path);
*out_count = 0;
return NULL;
close_and_exit:
closedir(d);
*out_count = i;
return list;
close_and_exit:
closedir(d);
*out_count = i;
return list;
#endif
}
}
#if !defined(__linux__) && !defined(_WIN32)
static void update_watches(platform_watch_context_t *ctx)
@@ -665,7 +662,7 @@ stk_module_event_t *platform_directory_watch_check(
int fd = (int)(long)handle;
char buf[STK_EVENT_BUFFER];
ssize_t len;
size_t index = 0, count = 0, i;
size_t index = 0, count = 0, i, write_index;
stk_module_event_t *evs;
char *ptr, *end;
struct inotify_event *e;
@@ -763,6 +760,19 @@ stk_module_event_t *platform_directory_watch_check(
}
}
write_index = 0;
for (i = 0; i < index; ++i) {
if (evs[i] != -1) {
if (write_index != i) {
evs[write_index] = evs[i];
memmove((*file_list)[write_index],
(*file_list)[i], STK_PATH_MAX);
}
write_index++;
}
}
index = write_index;
*out_count = index;
return evs;
+20 -11
View File
@@ -172,8 +172,7 @@ size_t stk_poll(void)
char (*file_list)[STK_PATH_MAX] = NULL;
stk_module_event_t *events = NULL;
size_t i, file_count = 0, reload_count = 0, load_count = 0,
unload_count = 0, reload_index = 0, load_index = 0,
unload_index = 0;
unload_count = 0;
int *reloaded_mod_indices = NULL, *reloaded_mod_file_indices = NULL,
*unloaded_mod_indices = NULL, *loaded_mod_indices = NULL;
size_t remaining_loads, new_capacity, holes_to_fill;
@@ -208,20 +207,31 @@ size_t stk_poll(void)
unloaded_mod_indices = malloc(unload_count * sizeof(int));
loaded_mod_indices = malloc(load_count * sizeof(int));
reload_count = 0;
unload_count = 0;
load_count = 0;
for (i = 0; i < file_count; ++i) {
int mod_index;
extract_module_id(file_list[i], mod_id);
switch (events[i]) {
case STK_MOD_LOAD:
loaded_mod_indices[load_index++] = i;
loaded_mod_indices[load_count++] = i;
break;
case STK_MOD_RELOAD:
reloaded_mod_file_indices[reload_index] = i;
reloaded_mod_indices[reload_index++] =
is_mod_loaded(mod_id);
mod_index = is_mod_loaded(mod_id);
if (mod_index >= 0) {
reloaded_mod_file_indices[reload_count] = i;
reloaded_mod_indices[reload_count] = mod_index;
reload_count++;
}
break;
case STK_MOD_UNLOAD:
unloaded_mod_indices[unload_index++] =
is_mod_loaded(mod_id);
mod_index = is_mod_loaded(mod_id);
if (mod_index >= 0) {
unloaded_mod_indices[unload_count] = mod_index;
unload_count++;
}
break;
}
}
@@ -242,9 +252,6 @@ begin_operations:
for (i = 0; i < unload_count; ++i)
stk_module_unload(unloaded_mod_indices[i]);
for (i = 0; i < reload_count; ++i)
stk_module_unload(reloaded_mod_indices[i]);
for (i = 0; i < reload_count; ++i) {
int file_index = reloaded_mod_file_indices[i];
int mod_index = reloaded_mod_indices[i];
@@ -261,6 +268,8 @@ begin_operations:
continue;
}
stk_module_unload(mod_index);
load_result = stk_module_load(tmp_path, mod_index);
if (load_result != STK_MOD_INIT_SUCCESS) {
stk_log(stderr, "Failed to reload module %s: %s",