Build
* cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
winpthreads and ncurses 6.6 are linked statically, so the release is
cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
* windres embeds a manifest with activeCodePage=UTF-8 and a version resource
* ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
wide-char declarations): scripts/patches/
* scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
UCRT64; PR #1's MINGW64 notes now point there
* vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW
UTF-8
* paths cross the system call boundary as UTF-16 (*W APIs); console code
pages set to 65001 and restored on exit; files are read and written in
binary mode
* child output that is not valid UTF-8 is decoded from the console, OEM or
ANSI system code page
* read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
endings and a UTF-8 BOM
OS layer and processes
* os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
subagent, plugins, skills, undo and fs_tools; realpath / path_under and
symlink rejection work on Windows
* every child goes through CreateProcessW with its own environment block
inside a Job Object, so timeouts and Esc end the whole process tree
Sandbox (auto mode, "auto · lowil")
* run_shell runs with a restricted Low integrity token; the workspace gets
an inheritable Low label once, the sandbox TEMP is labeled Low, and the
credentials file gets a no-read-up label
* drive roots, the profile and system folders are never labeled
* network commands still ask, since Low IL does not block the network;
Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
* /sandbox lists labeled folders, /sandbox reset-labels restores them
TUI
* raster console fonts are switched to Consolas / NSimSun while running
* the home logo falls back to text when the console font lacks U+2580
* mouse drag copy goes to the Windows clipboard
Distribution
* scripts/install_windows.ps1 installs a Gitea release into
%LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
zip with the token taken from the environment
* scripts/run_windows.bat is gone; docs/README-windows.md is rewritten
Tests
* new test_lowil, test_win_utf8 and test_os_dir; tests made portable
(cst_test_util.h)
* scripts/win_test.sh and scripts/winvm/ run the suite on English and
Chinese Windows Server VMs as Administrator, a standard user and a
Chinese user name: 29/29 on all six
185 lines
6.2 KiB
C
185 lines
6.2 KiB
C
/* Windows Low-IL shell sandbox: writes only in the workspace and the
|
|
* sandbox TEMP, the credentials file is unreadable, timeouts end the whole
|
|
* process tree, and /sandbox reset-labels restores the workspace label.
|
|
* Elsewhere it only checks that the kind is never reported. */
|
|
#ifndef _GNU_SOURCE
|
|
#define _GNU_SOURCE
|
|
#endif
|
|
|
|
#include "cst_sandbox.h"
|
|
#include "os_compat.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "cst_test_util.h"
|
|
|
|
static int nfail;
|
|
|
|
#define EXPECT(cond, msg) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
|
|
nfail++; \
|
|
} \
|
|
} while (0)
|
|
|
|
#ifdef _WIN32
|
|
static int run(const char *ws, const char *cmd, os_run_result_t *r) {
|
|
memset(r, 0, sizeof(*r));
|
|
if (cst_sandbox_run_shell(ws, cmd, 0, r) != 0)
|
|
return -1;
|
|
return r->exit_code;
|
|
}
|
|
|
|
static int has(const os_run_result_t *r, const char *s) {
|
|
return r->out && strstr(r->out, s);
|
|
}
|
|
|
|
static void write_file(const char *path, const char *text) {
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) {
|
|
fputs(text, f);
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
static void confined(void) {
|
|
char root[1024], cfg[1100], ws[1100], out[1100], cred[1300], cmd[4096];
|
|
char wsr[1100];
|
|
if (test_tmpdir(root, sizeof(root), "cst-lowil") != 0) {
|
|
EXPECT(0, "tmpdir");
|
|
return;
|
|
}
|
|
snprintf(cfg, sizeof(cfg), "%s/cfg", root);
|
|
snprintf(ws, sizeof(ws), "%s/项目 ws", root);
|
|
snprintf(out, sizeof(out), "%s/outside", root);
|
|
test_mkdir(cfg, 0700);
|
|
test_mkdir(ws, 0755);
|
|
test_mkdir(out, 0755);
|
|
setenv("XDG_CONFIG_HOME", cfg, 1);
|
|
char cdir[1200];
|
|
EXPECT(os_config_dir(cdir, sizeof(cdir)) == 0, "config dir");
|
|
os_mkdir_p(cdir, 0700);
|
|
snprintf(cred, sizeof(cred), "%s/credentials", cdir);
|
|
write_file(cred, "deepseek=sk-test-secret\n");
|
|
EXPECT(os_chmod_private(cred) == 0, "label credentials");
|
|
EXPECT(os_realpath(ws, wsr, sizeof(wsr)) == 0, "realpath ws");
|
|
os_path_norm_slashes(wsr);
|
|
|
|
os_run_result_t r;
|
|
EXPECT(run(wsr, "echo hi > in.txt && cat in.txt", &r) == 0,
|
|
"write inside the workspace");
|
|
EXPECT(has(&r, "hi"), "output captured");
|
|
EXPECT(has(&r, "labeled"), "first run says the workspace was labeled");
|
|
os_run_result_free(&r);
|
|
|
|
EXPECT(run(wsr, "echo again > in.txt && cat in.txt", &r) == 0,
|
|
"second run");
|
|
EXPECT(!has(&r, "labeled"), "labeled only once");
|
|
EXPECT(!has(&r, "background"), "conhost is not a background process");
|
|
os_run_result_free(&r);
|
|
|
|
char *labels = cst_sandbox_labels();
|
|
EXPECT(labels && strstr(labels, wsr), "workspace recorded");
|
|
free(labels);
|
|
|
|
snprintf(cmd, sizeof(cmd), "echo x > '%s/o.txt'", out);
|
|
int rc = run(wsr, cmd, &r);
|
|
EXPECT(rc != 0, "write outside the workspace fails");
|
|
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "outside write blocked");
|
|
os_run_result_free(&r);
|
|
snprintf(cmd, sizeof(cmd), "%s/o.txt", out);
|
|
os_stat_t st;
|
|
EXPECT(os_lstat(cmd, &st) != 0, "nothing written outside");
|
|
|
|
snprintf(cmd, sizeof(cmd), "cat '%s'", cred);
|
|
rc = run(wsr, cmd, &r);
|
|
EXPECT(rc != 0 && !has(&r, "sk-test-secret"), "credentials unreadable");
|
|
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "credential read blocked");
|
|
os_run_result_free(&r);
|
|
|
|
EXPECT(run(wsr, "echo t > \"$TEMP/t.txt\" && cat \"$TEMP/t.txt\"", &r) == 0,
|
|
"TEMP is writable");
|
|
EXPECT(has(&r, "t"), "TEMP read back");
|
|
os_run_result_free(&r);
|
|
|
|
/* System32's whoami: busybox has an applet and MSYS2's cannot start at
|
|
* Low IL */
|
|
char sys[MAX_PATH];
|
|
UINT sl = GetSystemDirectoryA(sys, sizeof(sys));
|
|
for (UINT i = 0; i < sl; i++)
|
|
if (sys[i] == '\\')
|
|
sys[i] = '/';
|
|
snprintf(cmd, sizeof(cmd), "'%s/whoami.exe' /groups | grep S-1-16-4096",
|
|
sys);
|
|
rc = run(wsr, cmd, &r);
|
|
EXPECT(rc == 0, "child runs at Low integrity");
|
|
if (rc != 0)
|
|
fprintf(stderr, "whoami: exit %d: %.*s\n", rc, (int)r.out_len,
|
|
r.out ? r.out : "");
|
|
os_run_result_free(&r);
|
|
|
|
/* timeout ends background children too */
|
|
os_set_run_timeout(2);
|
|
double t0 = os_now_ms();
|
|
rc = run(wsr, "sleep 30 & sleep 30; echo late", &r);
|
|
double ms = os_now_ms() - t0;
|
|
EXPECT(rc == 124, "timeout exit code");
|
|
EXPECT(ms < 10000, "process tree killed promptly");
|
|
EXPECT(!has(&r, "late"), "command did not finish");
|
|
os_run_result_free(&r);
|
|
os_set_run_timeout(0);
|
|
|
|
const char *prof = getenv("USERPROFILE");
|
|
char profr[1100];
|
|
if (prof && os_realpath(prof, profr, sizeof(profr)) == 0) {
|
|
os_path_norm_slashes(profr);
|
|
rc = run(profr, "echo no", &r);
|
|
EXPECT(rc == 126 && has(&r, "will not label"), "profile is not labeled");
|
|
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "offers a rerun");
|
|
os_run_result_free(&r);
|
|
}
|
|
rc = run("C:/", "echo no", &r);
|
|
EXPECT(rc == 126 && has(&r, "will not label"), "drive root is not labeled");
|
|
os_run_result_free(&r);
|
|
|
|
char *rep = NULL;
|
|
EXPECT(cst_sandbox_reset_labels(&rep) >= 1, "reset-labels restored");
|
|
EXPECT(rep && strstr(rep, "restored"), "reset report");
|
|
free(rep);
|
|
labels = cst_sandbox_labels();
|
|
EXPECT(!labels, "record cleared");
|
|
free(labels);
|
|
char sddl[256];
|
|
EXPECT(os_win_get_label(wsr, sddl, sizeof(sddl)) == 0 &&
|
|
strcmp(sddl, "S:") == 0,
|
|
"workspace has no explicit label again");
|
|
|
|
unsetenv("XDG_CONFIG_HOME");
|
|
test_rmrf(root);
|
|
}
|
|
#endif
|
|
|
|
int main(void) {
|
|
int kind = cst_sandbox_probe();
|
|
printf("test_lowil: sandbox %s\n", cst_sandbox_name(kind));
|
|
EXPECT(strcmp(cst_sandbox_name(CST_SANDBOX_LOWIL), "lowil") == 0, "name");
|
|
#ifdef _WIN32
|
|
EXPECT(kind == CST_SANDBOX_LOWIL, "lowil available");
|
|
if (kind == CST_SANDBOX_LOWIL)
|
|
confined();
|
|
#else
|
|
EXPECT(kind != CST_SANDBOX_LOWIL, "lowil is Windows only");
|
|
char *rep = NULL;
|
|
EXPECT(cst_sandbox_reset_labels(&rep) == 0 && !rep, "nothing to reset");
|
|
EXPECT(!cst_sandbox_labels(), "no labels");
|
|
#endif
|
|
if (nfail) {
|
|
fprintf(stderr, "test_lowil: %d failure(s)\n", nfail);
|
|
return 1;
|
|
}
|
|
puts("test_lowil: OK");
|
|
return 0;
|
|
}
|