From 8f2766ae9fd2ab7ac41de69a1a4cfd986c38bd96 Mon Sep 17 00:00:00 2001 From: ithewei Date: Tue, 15 Sep 2026 13:43:27 +0800 Subject: [PATCH 1/2] fix(http): harden FileCache against short read and stat failure - FileCache::Open: on a short read the entry was already put() into the LRU with filebuf.len == st_size, so is_complete() would later report it as complete and serve a partially-filled (garbage-tail) buffer. Remove the entry from the cache on read failure. - file_cache_s::is_modified: stat() result was written directly into st without checking the return value; on failure POSIX leaves the buffer undefined, corrupting st_size/st_mtime (and thus is_complete/etag/ Last-Modified). Stat into a temporary and keep the old st on failure. Co-authored-by: TRAE CLI --- http/server/FileCache.cpp | 1 + http/server/FileCache.h | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index c8b6897af..6f2d1dfd5 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -108,6 +108,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { if (nread != fc->filebuf.len) { hloge("Failed to read file: %s", filepath); param->error = ERR_READ_FILE; + Close(filepath); return NULL; } } diff --git a/http/server/FileCache.h b/http/server/FileCache.h index 2e0d51aff..36eb5cb2f 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -32,8 +32,13 @@ typedef struct file_cache_s { } bool is_modified() { + struct stat new_st; + // keep old st if stat failed (POSIX leaves the buffer undefined) + if (stat(filepath.c_str(), &new_st) != 0) { + return false; + } time_t mtime = st.st_mtime; - stat(filepath.c_str(), &st); + st = new_st; return mtime != st.st_mtime; } From 22278d97c4e60772470615f4268cc94cfd9226b9 Mon Sep 17 00:00:00 2001 From: ithewei Date: Tue, 15 Sep 2026 14:29:58 +0800 Subject: [PATCH 2/2] refactor(http): dedup FileCache stat/open across platforms Extract stat_and_open() to hide the OS_WIN vs POSIX branching that was duplicated in FileCache::Open, and route is_modified() through the same _wstat/stat split. is_modified() now returns true on stat failure so a removed/failed file forces a reopen (which then reports the error cleanly) instead of serving stale cached content. Co-authored-by: TRAE CLI --- http/server/FileCache.cpp | 47 ++++++++++++++------------------------- http/server/FileCache.h | 12 ++++++---- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index 6f2d1dfd5..143d0034d 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -16,6 +16,22 @@ #define ETAG_FMT "\"%zx-%zx\"" +// platform-abstracted stat + open. +// @return fd (>=0) on success, -1 on error. fills st on success. +// NOTE: open(dir) returns -1 on windows, so a directory yields fd=0 there. +static int stat_and_open(const char* filepath, struct stat* st, int flags) { +#ifdef OS_WIN + std::wstring wpath = hv::utf8_to_wchar(filepath); + if (_wstat(wpath.c_str(), (struct _stat*)st) != 0) return -1; + if (S_ISREG(st->st_mode)) return _wopen(wpath.c_str(), flags); + if (S_ISDIR(st->st_mode)) return 0; + return -1; +#else + if (stat(filepath, st) != 0) return -1; + return open(filepath, flags); +#endif +} + FileCache::FileCache(size_t capacity) : hv::LRUCache(capacity) { stat_interval = 10; // s expired_time = 60; // s @@ -23,23 +39,13 @@ FileCache::FileCache(size_t capacity) : hv::LRUCachestat_time > stat_interval) { fc->stat_time = now; fc->stat_cnt++; -#ifdef OS_WIN - wfilepath = hv::utf8_to_wchar(filepath); - now = fc->st.st_mtime; - _wstat(wfilepath.c_str(), (struct _stat*)&fc->st); - modified = now != fc->st.st_mtime; -#else modified = fc->is_modified(); -#endif } if (param->need_read) { if (!modified && fc->is_complete()) { @@ -53,26 +59,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { #ifdef O_BINARY flags |= O_BINARY; #endif - int fd = -1; -#ifdef OS_WIN - if(wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); - if(_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { - param->error = ERR_OPEN_FILE; - return NULL; - } - if(S_ISREG(st.st_mode)) { - fd = _wopen(wfilepath.c_str(), flags); - }else if (S_ISDIR(st.st_mode)) { - // NOTE: open(dir) return -1 on windows - fd = 0; - } -#else - if(stat(filepath, &st) != 0) { - param->error = ERR_OPEN_FILE; - return NULL; - } - fd = open(filepath, flags); -#endif + int fd = stat_and_open(filepath, &st, flags); if (fd < 0) { param->error = ERR_OPEN_FILE; return NULL; diff --git a/http/server/FileCache.h b/http/server/FileCache.h index 36eb5cb2f..2f53c06a6 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -33,10 +33,14 @@ typedef struct file_cache_s { bool is_modified() { struct stat new_st; - // keep old st if stat failed (POSIX leaves the buffer undefined) - if (stat(filepath.c_str(), &new_st) != 0) { - return false; - } + // stat failed: treat as modified to force re-validation via reopen +#ifdef OS_WIN + if (_wstat(hv::utf8_to_wchar(filepath).c_str(), (struct _stat*)&new_st) != 0) + return true; +#else + if (stat(filepath.c_str(), &new_st) != 0) + return true; +#endif time_t mtime = st.st_mtime; st = new_st; return mtime != st.st_mtime;