Temp dir falls back when TMPDIR or /tmp is unusable (Termux); v0.6.1
os_temp_dir() used $TMPDIR or "/tmp" without checking either. On Termux
/tmp usually does not exist (or SELinux denies it) unless TMPDIR points at
$PREFIX/tmp, so plugin and MCP temp files failed and shell children that
need a temp file (heredocs) broke.
* os_temp_dir() picks, once, the first of $TMPDIR, $PREFIX/tmp, /tmp,
/var/tmp and ~/.cache/cst-agent/tmp where a file can really be
created; if none works it keeps the old answer
* os_temp_dir_export() at startup writes the choice back to TMPDIR, so
shell children and the sandbox's writable temp dir agree
* tests/test_tmpdir.c covers the fallbacks in fresh forks
This commit is contained in:
+1
-1
@@ -256,7 +256,7 @@ foreach(t test_llm_body test_sse_decode test_apply_patch test_session_roundtrip
|
|||||||
test_undo test_config test_models test_sandbox
|
test_undo test_config test_models test_sandbox
|
||||||
test_keymap test_shell_compact test_skills
|
test_keymap test_shell_compact test_skills
|
||||||
test_plugins test_mcp test_subagent test_lowil
|
test_plugins test_mcp test_subagent test_lowil
|
||||||
test_win_utf8 test_os_dir)
|
test_win_utf8 test_os_dir test_tmpdir)
|
||||||
add_executable(${t} tests/${t}.c ${CST_CORE_SOURCES})
|
add_executable(${t} tests/${t}.c ${CST_CORE_SOURCES})
|
||||||
target_include_directories(${t} PRIVATE include "${CJSON_DIR}")
|
target_include_directories(${t} PRIVATE include "${CJSON_DIR}")
|
||||||
target_compile_definitions(${t} PRIVATE _GNU_SOURCE)
|
target_compile_definitions(${t} PRIVATE _GNU_SOURCE)
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ TESTS = \
|
|||||||
tests/test_subagent \
|
tests/test_subagent \
|
||||||
tests/test_lowil \
|
tests/test_lowil \
|
||||||
tests/test_win_utf8 \
|
tests/test_win_utf8 \
|
||||||
tests/test_os_dir
|
tests/test_os_dir \
|
||||||
|
tests/test_tmpdir
|
||||||
|
|
||||||
TEST_LIBS = -lm $(CURL_LIBS) $(PTHREAD_LIBS)
|
TEST_LIBS = -lm $(CURL_LIBS) $(PTHREAD_LIBS)
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ make && make install # 装到 Termux 的 $PREFIX/bin,之后任意目录
|
|||||||
```
|
```
|
||||||
|
|
||||||
环境:`PREFIX` 自动用于找 `sh`;`CST_EXE_DIR` / `argv[0]` 用于定位旁路 busybox(`/proc/self/exe` 受限时)。
|
环境:`PREFIX` 自动用于找 `sh`;`CST_EXE_DIR` / `argv[0]` 用于定位旁路 busybox(`/proc/self/exe` 受限时)。
|
||||||
|
临时目录:依次试 `$TMPDIR`、`$PREFIX/tmp`、`/tmp`、`/var/tmp`、`~/.cache/cst-agent/tmp`,用第一个真能建文件的,
|
||||||
|
并写回 `TMPDIR` 给 shell 子进程和沙箱用(Termux 的 `/tmp` 常常不存在或被 SELinux 拒绝)。
|
||||||
|
|
||||||
### Windows 发行包
|
### Windows 发行包
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#define CST_VERSION_MAJOR 0
|
#define CST_VERSION_MAJOR 0
|
||||||
#define CST_VERSION_MINOR 6
|
#define CST_VERSION_MINOR 6
|
||||||
#define CST_VERSION_PATCH 0
|
#define CST_VERSION_PATCH 1
|
||||||
|
|
||||||
#define CST_VERSION_STR_(x) #x
|
#define CST_VERSION_STR_(x) #x
|
||||||
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ int os_mkdir(const char *path, int mode);
|
|||||||
int os_mkdir_p(const char *path, int mode);
|
int os_mkdir_p(const char *path, int mode);
|
||||||
int os_config_dir(char *out, size_t out_sz);
|
int os_config_dir(char *out, size_t out_sz);
|
||||||
int os_temp_dir(char *out, size_t out_sz);
|
int os_temp_dir(char *out, size_t out_sz);
|
||||||
|
/* Point TMPDIR at os_temp_dir() so child processes use the same writable
|
||||||
|
* directory. Call once at startup, before any threads. */
|
||||||
|
void os_temp_dir_export(void);
|
||||||
/* Create unique temp file; writes path into out; returns fd or -1. Caller closes. */
|
/* Create unique temp file; writes path into out; returns fd or -1. Caller closes. */
|
||||||
int os_mkstemp(char *out, size_t out_sz, const char *prefix);
|
int os_mkstemp(char *out, size_t out_sz, const char *prefix);
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ int main(int argc, char **argv) {
|
|||||||
fprintf(stderr, "warning: the process code page is not UTF-8 (Windows 10 "
|
fprintf(stderr, "warning: the process code page is not UTF-8 (Windows 10 "
|
||||||
"1903 or later is required); non-ASCII text may break\n");
|
"1903 or later is required); non-ASCII text may break\n");
|
||||||
os_set_prog_path(argv[0]);
|
os_set_prog_path(argv[0]);
|
||||||
|
os_temp_dir_export();
|
||||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
cst_agent_config_t cfg = {0};
|
cst_agent_config_t cfg = {0};
|
||||||
|
|||||||
+71
-4
@@ -255,6 +255,66 @@ int os_config_dir(char *out, size_t out_sz) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
/* $TMPDIR is not always usable: Termux has no /tmp unless TMPDIR points at
|
||||||
|
* $PREFIX/tmp, and Android may deny /tmp or /data/local/tmp outright. Take
|
||||||
|
* the first candidate where a file can actually be created. */
|
||||||
|
static char g_tmp[OS_PATH_MAX];
|
||||||
|
static pthread_once_t g_tmp_once = PTHREAD_ONCE_INIT;
|
||||||
|
|
||||||
|
static int tmp_writable(const char *dir) {
|
||||||
|
char probe[OS_PATH_MAX];
|
||||||
|
struct stat st;
|
||||||
|
if (!dir || dir[0] != '/' || stat(dir, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||||
|
return 0;
|
||||||
|
int n = snprintf(probe, sizeof(probe), "%s/.cst-probe-XXXXXX", dir);
|
||||||
|
if (n < 0 || (size_t)n >= sizeof(probe))
|
||||||
|
return 0;
|
||||||
|
int fd = mkstemp(probe);
|
||||||
|
if (fd < 0)
|
||||||
|
return 0;
|
||||||
|
close(fd);
|
||||||
|
unlink(probe);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tmp_trim(char *s) {
|
||||||
|
size_t n = strlen(s);
|
||||||
|
while (n > 1 && s[n - 1] == '/')
|
||||||
|
s[--n] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tmp_pick(void) {
|
||||||
|
char cand[5][OS_PATH_MAX];
|
||||||
|
int nc = 0;
|
||||||
|
const char *env = getenv("TMPDIR");
|
||||||
|
const char *prefix = getenv("PREFIX");
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
const char *cache = getenv("XDG_CACHE_HOME");
|
||||||
|
if (env && env[0])
|
||||||
|
snprintf(cand[nc++], OS_PATH_MAX, "%s", env);
|
||||||
|
if (prefix && prefix[0] == '/')
|
||||||
|
snprintf(cand[nc++], OS_PATH_MAX, "%s/tmp", prefix);
|
||||||
|
snprintf(cand[nc++], OS_PATH_MAX, "/tmp");
|
||||||
|
snprintf(cand[nc++], OS_PATH_MAX, "/var/tmp");
|
||||||
|
if (cache && cache[0] == '/')
|
||||||
|
snprintf(cand[nc++], OS_PATH_MAX, "%s/cst-agent/tmp", cache);
|
||||||
|
else if (home && home[0] == '/')
|
||||||
|
snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/cst-agent/tmp", home);
|
||||||
|
for (int i = 0; i < nc; i++) {
|
||||||
|
tmp_trim(cand[i]);
|
||||||
|
if (i == nc - 1)
|
||||||
|
os_mkdir_p(cand[i], 0700);
|
||||||
|
if (tmp_writable(cand[i])) {
|
||||||
|
memcpy(g_tmp, cand[i], strlen(cand[i]) + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* nothing works: keep the old answer so callers fail as before */
|
||||||
|
memcpy(g_tmp, cand[0], strlen(cand[0]) + 1);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int os_temp_dir(char *out, size_t out_sz) {
|
int os_temp_dir(char *out, size_t out_sz) {
|
||||||
if (!out || out_sz < 2)
|
if (!out || out_sz < 2)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -268,11 +328,18 @@ int os_temp_dir(char *out, size_t out_sz) {
|
|||||||
out[--L] = 0;
|
out[--L] = 0;
|
||||||
return 0;
|
return 0;
|
||||||
#else
|
#else
|
||||||
|
pthread_once(&g_tmp_once, tmp_pick);
|
||||||
|
int n = snprintf(out, out_sz, "%s", g_tmp);
|
||||||
|
return n < 0 || (size_t)n >= out_sz ? -1 : 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void os_temp_dir_export(void) {
|
||||||
|
#ifndef _WIN32
|
||||||
|
char d[OS_PATH_MAX];
|
||||||
const char *t = getenv("TMPDIR");
|
const char *t = getenv("TMPDIR");
|
||||||
if (!t || !t[0])
|
if (os_temp_dir(d, sizeof(d)) == 0 && (!t || strcmp(t, d) != 0))
|
||||||
t = "/tmp";
|
setenv("TMPDIR", d, 1);
|
||||||
snprintf(out, out_sz, "%s", t);
|
|
||||||
return 0;
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/* os_temp_dir() falls back when $TMPDIR is missing or read-only (Termux,
|
||||||
|
* Android) and os_temp_dir_export() hands the choice to child processes.
|
||||||
|
* Each case runs in a fresh fork because the choice is made once. */
|
||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static int nfail;
|
||||||
|
|
||||||
|
#define EXPECT(cond, msg) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
|
||||||
|
nfail++; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* Runs in a child: set the environment, then compare os_temp_dir(). */
|
||||||
|
static int pick_is(const char *tmpdir, const char *prefix, const char *want) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0)
|
||||||
|
return 0;
|
||||||
|
if (pid == 0) {
|
||||||
|
if (tmpdir)
|
||||||
|
setenv("TMPDIR", tmpdir, 1);
|
||||||
|
else
|
||||||
|
unsetenv("TMPDIR");
|
||||||
|
if (prefix)
|
||||||
|
setenv("PREFIX", prefix, 1);
|
||||||
|
else
|
||||||
|
unsetenv("PREFIX");
|
||||||
|
char got[4096];
|
||||||
|
if (os_temp_dir(got, sizeof(got)) != 0 || strcmp(got, want) != 0) {
|
||||||
|
fprintf(stderr, " TMPDIR=%s PREFIX=%s: got %s, want %s\n",
|
||||||
|
tmpdir ? tmpdir : "(unset)", prefix ? prefix : "(unset)", got,
|
||||||
|
want);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
os_temp_dir_export();
|
||||||
|
const char *t = getenv("TMPDIR");
|
||||||
|
if (!t || strcmp(t, want) != 0)
|
||||||
|
_exit(2);
|
||||||
|
char f[4096];
|
||||||
|
int fd = os_mkstemp(f, sizeof(f), "cst-t");
|
||||||
|
if (fd < 0 || strncmp(f, want, strlen(want)) != 0)
|
||||||
|
_exit(3);
|
||||||
|
close(fd);
|
||||||
|
unlink(f);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
int st = 0;
|
||||||
|
waitpid(pid, &st, 0);
|
||||||
|
return WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
const char *base = getenv("TMPDIR");
|
||||||
|
char root[4096];
|
||||||
|
snprintf(root, sizeof(root), "%s/cst-tmpdir-XXXXXX",
|
||||||
|
base && base[0] ? base : "/tmp");
|
||||||
|
if (!mkdtemp(root)) {
|
||||||
|
puts("test_tmpdir: skipped (no writable temp dir to set up in)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char ok[4200], prefix[4200], ptmp[4300], ro[4200], slash[4300];
|
||||||
|
snprintf(ok, sizeof(ok), "%s/ok", root);
|
||||||
|
snprintf(prefix, sizeof(prefix), "%s/usr", root);
|
||||||
|
snprintf(ptmp, sizeof(ptmp), "%s/tmp", prefix);
|
||||||
|
snprintf(ro, sizeof(ro), "%s/ro", root);
|
||||||
|
snprintf(slash, sizeof(slash), "%s//", ok);
|
||||||
|
mkdir(ok, 0700);
|
||||||
|
mkdir(prefix, 0700);
|
||||||
|
mkdir(ptmp, 0700);
|
||||||
|
mkdir(ro, 0500);
|
||||||
|
|
||||||
|
EXPECT(pick_is(ok, NULL, ok), "usable TMPDIR wins");
|
||||||
|
EXPECT(pick_is(slash, NULL, ok), "trailing slashes trimmed");
|
||||||
|
EXPECT(pick_is("/nonexistent/cst", prefix, ptmp),
|
||||||
|
"missing TMPDIR falls back to $PREFIX/tmp");
|
||||||
|
EXPECT(pick_is(NULL, prefix, ptmp), "unset TMPDIR uses $PREFIX/tmp");
|
||||||
|
if (geteuid() != 0) {
|
||||||
|
EXPECT(pick_is(ro, prefix, ptmp), "read-only TMPDIR is skipped");
|
||||||
|
char tmp_ok[64] = "/tmp";
|
||||||
|
if (access("/tmp", W_OK) == 0)
|
||||||
|
EXPECT(pick_is(ro, NULL, tmp_ok), "read-only TMPDIR falls to /tmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir(ro);
|
||||||
|
rmdir(ptmp);
|
||||||
|
rmdir(prefix);
|
||||||
|
rmdir(ok);
|
||||||
|
rmdir(root);
|
||||||
|
if (nfail) {
|
||||||
|
fprintf(stderr, "test_tmpdir: %d failure(s)\n", nfail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
puts("test_tmpdir: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int main(void) {
|
||||||
|
puts("test_tmpdir: skipped on Windows");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user