Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
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
This commit is contained in:
@@ -19,6 +19,9 @@
|
||||
#elif defined(__APPLE__)
|
||||
#define CST_HAVE_SEATBELT 1
|
||||
#include <stdint.h>
|
||||
#elif defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#include <sddl.h>
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && (defined(__x86_64__) || defined(__aarch64__))
|
||||
@@ -864,6 +867,341 @@ static int sc_probe(void) {
|
||||
|
||||
#endif /* CST_HAVE_SECCOMP */
|
||||
|
||||
/* --- Windows: Low integrity level ----------------------------------------- */
|
||||
#ifdef _WIN32
|
||||
|
||||
#define LW_LABEL "S:(ML;OICI;NW;;;LW)"
|
||||
#define LW_RECORD "sandbox-labels"
|
||||
|
||||
/* Same user, no admin privileges, Low IL. No LUA_TOKEN: a deny-only
|
||||
* Administrators group cannot open an admin's (SSH) window station and the
|
||||
* child fails in DLL init; the integrity check does not need it. */
|
||||
static HANDLE lw_token(void) {
|
||||
HANDLE self = NULL, tok = NULL;
|
||||
if (!OpenProcessToken(GetCurrentProcess(),
|
||||
TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT |
|
||||
TOKEN_ASSIGN_PRIMARY,
|
||||
&self))
|
||||
return NULL;
|
||||
BOOL ok = CreateRestrictedToken(self, DISABLE_MAX_PRIVILEGE, 0, NULL, 0,
|
||||
NULL, 0, NULL, &tok);
|
||||
CloseHandle(self);
|
||||
if (!ok)
|
||||
return NULL;
|
||||
PSID low = NULL;
|
||||
if (!ConvertStringSidToSidW(L"S-1-16-4096", &low)) {
|
||||
CloseHandle(tok);
|
||||
return NULL;
|
||||
}
|
||||
TOKEN_MANDATORY_LABEL tml;
|
||||
memset(&tml, 0, sizeof(tml));
|
||||
tml.Label.Sid = low;
|
||||
tml.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
ok = SetTokenInformation(tok, TokenIntegrityLevel, &tml,
|
||||
(DWORD)(sizeof(tml) + GetLengthSid(low)));
|
||||
LocalFree(low);
|
||||
if (!ok) {
|
||||
CloseHandle(tok);
|
||||
return NULL;
|
||||
}
|
||||
return tok;
|
||||
}
|
||||
|
||||
static int lw_record_path(char *out, size_t n) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (os_config_dir(dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
return snprintf(out, n, "%s/%s", dir, LW_RECORD) < (int)n ? 0 : -1;
|
||||
}
|
||||
|
||||
static char *lw_read_record(void) {
|
||||
char path[OS_PATH_MAX];
|
||||
if (lw_record_path(path, sizeof(path)) != 0)
|
||||
return NULL;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return NULL;
|
||||
size_t cap = 4096, len = 0;
|
||||
char *s = malloc(cap);
|
||||
size_t k;
|
||||
while (s && (k = fread(s + len, 1, cap - len - 1, f)) > 0) {
|
||||
len += k;
|
||||
if (cap - len < 2) {
|
||||
char *g = realloc(s, cap * 2);
|
||||
if (!g) {
|
||||
free(s);
|
||||
s = NULL;
|
||||
break;
|
||||
}
|
||||
s = g;
|
||||
cap *= 2;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
if (s)
|
||||
s[len] = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* dir is a recorded directory or inside one (the label is inherited). */
|
||||
static int lw_recorded(const char *dir) {
|
||||
char *s = lw_read_record();
|
||||
int found = 0;
|
||||
for (char *line = s; line && *line && !found;) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl)
|
||||
*nl = 0;
|
||||
char *tab = strchr(line, '\t');
|
||||
if (tab)
|
||||
*tab = 0;
|
||||
found = line[0] && os_path_under(line, dir);
|
||||
line = nl ? nl + 1 : NULL;
|
||||
}
|
||||
free(s);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int lw_record_add(const char *dir, const char *old_sddl) {
|
||||
char path[OS_PATH_MAX];
|
||||
if (lw_record_path(path, sizeof(path)) != 0)
|
||||
return -1;
|
||||
FILE *f = fopen(path, "ab");
|
||||
if (!f)
|
||||
return -1;
|
||||
fprintf(f, "%s\t%s\n", dir, old_sddl);
|
||||
return fclose(f) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int lw_same_dir(const char *a, const char *b) {
|
||||
return a && b && a[0] && b[0] && os_path_under(a, b) && os_path_under(b, a);
|
||||
}
|
||||
|
||||
/* An environment directory in the form os_realpath gives the workspace
|
||||
* (long names, '/'), or NULL. */
|
||||
static const char *lw_env_dir(const char *name, char *buf, size_t n) {
|
||||
const char *v = getenv(name);
|
||||
if (!v || !v[0] || os_realpath(v, buf, n) != 0)
|
||||
return NULL;
|
||||
os_path_norm_slashes(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Labeling a whole drive, the profile or system folders would open far more
|
||||
* than a project to every sandboxed command. */
|
||||
static int lw_refused(const char *dir) {
|
||||
size_t n = strlen(dir);
|
||||
while (n > 0 && (dir[n - 1] == '/' || dir[n - 1] == '\\'))
|
||||
n--;
|
||||
if (n <= 2)
|
||||
return 1;
|
||||
static const char *const SELF[] = {"USERPROFILE", "APPDATA",
|
||||
"LOCALAPPDATA", "TEMP"};
|
||||
static const char *const TREE[] = {"SystemRoot", "ProgramFiles",
|
||||
"ProgramFiles(x86)", "ProgramData"};
|
||||
char buf[OS_PATH_MAX];
|
||||
for (size_t i = 0; i < sizeof(SELF) / sizeof(SELF[0]); i++)
|
||||
if (lw_same_dir(lw_env_dir(SELF[i], buf, sizeof(buf)), dir))
|
||||
return 1;
|
||||
for (size_t i = 0; i < sizeof(TREE) / sizeof(TREE[0]); i++) {
|
||||
const char *v = lw_env_dir(TREE[i], buf, sizeof(buf));
|
||||
if (v && os_path_under(v, dir))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lw_prepare(const char *ws, cst_sandbox_plan_t *p) {
|
||||
static int creds_done, tmp_done;
|
||||
char buf[OS_PATH_MAX + 256];
|
||||
if (!ws || !ws[0])
|
||||
return -1;
|
||||
if (!creds_done) {
|
||||
/* files saved by older versions have no no-read-up label yet */
|
||||
char dir[OS_PATH_MAX];
|
||||
if (os_config_dir(dir, sizeof(dir)) == 0 &&
|
||||
snprintf(buf, sizeof(buf), "%s/credentials", dir) < (int)sizeof(buf)) {
|
||||
os_stat_t st;
|
||||
if (os_lstat(buf, &st) != 0 || os_chmod_private(buf) == 0)
|
||||
creds_done = 1;
|
||||
}
|
||||
}
|
||||
char tmp[OS_PATH_MAX];
|
||||
if (os_temp_dir(tmp, sizeof(tmp)) != 0 ||
|
||||
snprintf(buf, sizeof(buf), "%s/cst-agent-sandbox", tmp) >=
|
||||
(int)sizeof(tmp))
|
||||
return -1;
|
||||
memcpy(tmp, buf, strlen(buf) + 1);
|
||||
if (!tmp_done) {
|
||||
os_mkdir_p(tmp, 0700);
|
||||
if (os_win_set_label(tmp, LW_LABEL) != 0)
|
||||
return -1;
|
||||
tmp_done = 1;
|
||||
}
|
||||
if (!lw_recorded(ws)) {
|
||||
char old[512];
|
||||
if (lw_refused(ws) || os_win_get_label(ws, old, sizeof(old)) != 0 ||
|
||||
os_win_set_label(ws, LW_LABEL) != 0)
|
||||
return -1;
|
||||
lw_record_add(ws, old);
|
||||
snprintf(buf, sizeof(buf),
|
||||
"[cst: sandbox: labeled %s Low integrity so sandboxed commands "
|
||||
"can write there; /sandbox reset-labels undoes it]\n",
|
||||
ws);
|
||||
p->note = strdup(buf);
|
||||
}
|
||||
HANDLE tok = lw_token();
|
||||
if (!tok) {
|
||||
free(p->note);
|
||||
p->note = NULL;
|
||||
return -1;
|
||||
}
|
||||
p->token = tok;
|
||||
for (char *c = tmp; *c; c++)
|
||||
if (*c == '/')
|
||||
*c = '\\';
|
||||
p->tmpdir = strdup(tmp);
|
||||
return p->tmpdir ? 0 : -1;
|
||||
}
|
||||
|
||||
static int lw_run(const char *ws, const char *cmd, size_t max_out,
|
||||
const cst_sandbox_plan_t *p, os_run_result_t *r) {
|
||||
size_t tl = strlen(p->tmpdir) + 8;
|
||||
char *te = malloc(tl), *tm = malloc(tl);
|
||||
int rc = -1;
|
||||
if (te && tm) {
|
||||
snprintf(te, tl, "TEMP=%s", p->tmpdir);
|
||||
snprintf(tm, tl, "TMP=%s", p->tmpdir);
|
||||
const char *env[] = {te, tm, NULL};
|
||||
rc = os_run_shell_token(ws, cmd, max_out, r, p->token, env);
|
||||
}
|
||||
free(te);
|
||||
free(tm);
|
||||
if (rc == 0 && p->note) {
|
||||
size_t nl = strlen(p->note);
|
||||
char *o = malloc(nl + r->out_len + 1);
|
||||
if (o) {
|
||||
memcpy(o, p->note, nl);
|
||||
if (r->out)
|
||||
memcpy(o + nl, r->out, r->out_len);
|
||||
o[nl + r->out_len] = 0;
|
||||
free(r->out);
|
||||
r->out = o;
|
||||
r->out_len += nl;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
char *cst_sandbox_labels(void) {
|
||||
#ifdef _WIN32
|
||||
char *s = lw_read_record();
|
||||
if (!s)
|
||||
return NULL;
|
||||
size_t o = 0;
|
||||
for (char *line = s; *line;) {
|
||||
char *nl = strchr(line, '\n');
|
||||
size_t len = nl ? (size_t)(nl - line) : strlen(line);
|
||||
size_t path = strcspn(line, "\t\n");
|
||||
if (path) {
|
||||
memmove(s + o, line, path);
|
||||
o += path;
|
||||
s[o++] = '\n';
|
||||
}
|
||||
line += len + (nl ? 1 : 0);
|
||||
}
|
||||
s[o] = 0;
|
||||
if (!o) {
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
return s;
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
int cst_sandbox_reset_labels(char **report) {
|
||||
if (report)
|
||||
*report = NULL;
|
||||
#ifdef _WIN32
|
||||
char *s = lw_read_record();
|
||||
size_t cap = 256, len = 0;
|
||||
char *rep = malloc(cap), *keep = NULL;
|
||||
size_t kcap = 0, klen = 0;
|
||||
int ok = 0, bad = 0;
|
||||
if (rep)
|
||||
rep[0] = 0;
|
||||
for (char *line = s; line && *line;) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl)
|
||||
*nl = 0;
|
||||
char *tab = strchr(line, '\t');
|
||||
const char *old = tab ? tab + 1 : "S:";
|
||||
if (tab)
|
||||
*tab = 0;
|
||||
if (line[0]) {
|
||||
os_stat_t st;
|
||||
int gone = os_lstat(line, &st) != 0;
|
||||
int done = gone || os_win_set_label(line, old[0] ? old : "S:") == 0;
|
||||
char msg[OS_PATH_MAX + 64];
|
||||
int m = snprintf(msg, sizeof(msg), "%s %s\n",
|
||||
gone ? "gone " : done ? "restored" : "FAILED ",
|
||||
line);
|
||||
if (m > 0 && rep) {
|
||||
while (len + (size_t)m + 1 > cap) {
|
||||
char *g = realloc(rep, cap *= 2);
|
||||
if (!g)
|
||||
break;
|
||||
rep = g;
|
||||
}
|
||||
if (len + (size_t)m + 1 <= cap) {
|
||||
memcpy(rep + len, msg, (size_t)m + 1);
|
||||
len += (size_t)m;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
ok++;
|
||||
} else {
|
||||
bad++;
|
||||
size_t need = strlen(line) + strlen(old) + 3;
|
||||
if (klen + need + 1 > kcap) {
|
||||
char *g = realloc(keep, kcap = (klen + need + 1) * 2);
|
||||
if (g)
|
||||
keep = g;
|
||||
}
|
||||
if (keep && klen + need + 1 <= kcap)
|
||||
klen += (size_t)snprintf(keep + klen, kcap - klen, "%s\t%s\n",
|
||||
line, old);
|
||||
}
|
||||
}
|
||||
line = nl ? nl + 1 : NULL;
|
||||
}
|
||||
free(s);
|
||||
char path[OS_PATH_MAX];
|
||||
if (lw_record_path(path, sizeof(path)) == 0) {
|
||||
if (klen) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (f) {
|
||||
fwrite(keep, 1, klen, f);
|
||||
fclose(f);
|
||||
}
|
||||
} else {
|
||||
remove(path);
|
||||
}
|
||||
}
|
||||
free(keep);
|
||||
if (report)
|
||||
*report = rep;
|
||||
else
|
||||
free(rep);
|
||||
return bad ? -1 : ok;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* --- common --------------------------------------------------------------- */
|
||||
|
||||
int cst_sandbox_probe(void) {
|
||||
@@ -891,6 +1229,15 @@ int cst_sandbox_probe(void) {
|
||||
#endif
|
||||
#ifdef CST_HAVE_SEATBELT
|
||||
kind = CST_SANDBOX_SEATBELT;
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (!e || strcmp(e, "lowil") == 0) {
|
||||
HANDLE tok = lw_token();
|
||||
if (tok) {
|
||||
CloseHandle(tok);
|
||||
kind = CST_SANDBOX_LOWIL;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
(void)want_ll;
|
||||
(void)want_sc;
|
||||
@@ -917,6 +1264,8 @@ const char *cst_sandbox_name(int kind) {
|
||||
return "seatbelt";
|
||||
case CST_SANDBOX_SECCOMP:
|
||||
return "seccomp";
|
||||
case CST_SANDBOX_LOWIL:
|
||||
return "lowil";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
@@ -939,6 +1288,10 @@ int cst_sandbox_prepare(const char *workspace, const char *tmpdir,
|
||||
#ifdef CST_HAVE_SECCOMP
|
||||
if (kind == CST_SANDBOX_SECCOMP)
|
||||
rc = sc_prepare(workspace, tmpdir, plan);
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (kind == CST_SANDBOX_LOWIL && (rc = lw_prepare(workspace, plan)) != 0)
|
||||
cst_sandbox_release(plan);
|
||||
#endif
|
||||
(void)workspace;
|
||||
(void)tmpdir;
|
||||
@@ -988,6 +1341,15 @@ void cst_sandbox_release(cst_sandbox_plan_t *plan) {
|
||||
#ifdef CST_HAVE_SECCOMP
|
||||
sc_free(plan->sc);
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (plan->token)
|
||||
CloseHandle(plan->token);
|
||||
#endif
|
||||
plan->token = NULL;
|
||||
free(plan->tmpdir);
|
||||
plan->tmpdir = NULL;
|
||||
free(plan->note);
|
||||
plan->note = NULL;
|
||||
plan->sc = NULL;
|
||||
free(plan->profile);
|
||||
plan->profile = NULL;
|
||||
@@ -1004,6 +1366,12 @@ int cst_sandbox_run_shell(const char *workspace, const char *cmd,
|
||||
if (cst_sandbox_prepare(workspace, tmp, &plan) != 0) {
|
||||
memset(r, 0, sizeof(*r));
|
||||
const char *msg = "[cst: sandbox unavailable; command not run]\n";
|
||||
#ifdef _WIN32
|
||||
if (workspace && !lw_recorded(workspace) && lw_refused(workspace))
|
||||
msg = "[cst: sandbox: will not label a drive root, the profile or a "
|
||||
"system folder writable; command not run. Start cst-agent in a "
|
||||
"project folder]\n";
|
||||
#endif
|
||||
r->out = malloc(strlen(msg) + 1);
|
||||
if (r->out) {
|
||||
memcpy(r->out, msg, strlen(msg) + 1);
|
||||
@@ -1012,8 +1380,12 @@ int cst_sandbox_run_shell(const char *workspace, const char *cmd,
|
||||
r->exit_code = 126;
|
||||
return 0;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
int rc = lw_run(workspace, cmd, max_out, &plan, r);
|
||||
#else
|
||||
int rc = os_run_shell_ex(workspace, cmd, max_out, r, cst_sandbox_enter,
|
||||
&plan);
|
||||
#endif
|
||||
cst_sandbox_release(&plan);
|
||||
return rc;
|
||||
}
|
||||
@@ -1039,6 +1411,11 @@ int cst_sandbox_blocked(const char *out, size_t n, int exit_code) {
|
||||
"权限不够", "不允许的操作", "只读文件系统", "權限不足",
|
||||
"拒絕不符權限的操作", "許可がありません", "Keine Berechtigung",
|
||||
"Permission non accord", "Permiso denegado", "Отказано в доступе",
|
||||
/* Windows ERROR_ACCESS_DENIED from cmd, PowerShell and Win32 tools */
|
||||
"Access is denied", "Access to the path", "UnauthorizedAccess",
|
||||
"拒绝访问", "存取被拒", "Zugriff verweigert", "Accès refusé",
|
||||
/* Cygwin/MSYS2 programs cannot initialize at Low IL */
|
||||
"DLL initialization routine failed", "0xc0000142",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(SIGNS) / sizeof(SIGNS[0]); i++)
|
||||
if (has(out, n, SIGNS[i]))
|
||||
|
||||
Reference in New Issue
Block a user