Harden dynamic HTTP and compiler boundaries

This commit is contained in:
root
2026-07-26 11:28:11 +00:00
parent 3d155203bd
commit d0efab7db0
30 changed files with 906 additions and 109 deletions
+154 -51
View File
@@ -39,6 +39,7 @@
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/file.h>
@@ -55,6 +56,7 @@
#include <sys/wait.h>
#include <signal.h>
#include <poll.h>
#include "hardened_http_internal.h"
struct WasmDylinkInfo
{
@@ -390,8 +392,6 @@ static u64 wasm_socket_connect_bounded(const String& host, u16 port, u64 timeout
}
if(fd <= 0)
return(0);
if(context)
context->resources.sockets.push_back(fd);
return((u64)fd);
}
@@ -622,7 +622,7 @@ static u64 uce_shell_spawn_spec(const DValue& spec)
}
static DValue uce_exec_argv_capture(std::vector<String> argv, String input, u64 timeout_ms)
static DValue uce_exec_argv_capture(std::vector<String> argv, String input, u64 timeout_ms, size_t output_limit=0, bool clean_env=false)
{
DValue r; r["exit_code"]=(f64)-1; r["stdout"]=""; r["stderr"]=""; r["timed_out"].set_bool(false);
if(argv.empty()) { r["stderr"]="empty argv"; return(r); }
@@ -639,7 +639,9 @@ static DValue uce_exec_argv_capture(std::vector<String> argv, String input, u64
dup2(inpipe[0],0); dup2(outpipe[1],1); dup2(errpipe[1],2);
close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); close(errpipe[0]); close(errpipe[1]);
std::vector<char*> args; for(auto& a: argv) args.push_back((char*)a.c_str()); args.push_back(0);
execvp(args[0], args.data()); _exit(127);
if(clean_env) { clearenv(); setenv("PATH", "/usr/bin:/bin", 1); execv(args[0], args.data()); }
else execvp(args[0], args.data());
_exit(127);
}
if(pid < 0)
{
@@ -655,8 +657,8 @@ static DValue uce_exec_argv_capture(std::vector<String> argv, String input, u64
{
if(!exited) { pid_t w=waitpid(pid,&status,WNOHANG); if(w==pid) { exited=true; status_valid=true; } else if(w<0&&errno==ECHILD) { u64 transfer_deadline=wasm_deadline_after_ms(50); do { status_valid=child_exit_status_take(pid,status,child_status_snapshot); if(!status_valid)sched_yield(); } while(!status_valid&&wasm_monotonic_ms()<transfer_deadline); exited=true; if(!status_valid)r["stderr"]=r["stderr"].to_string()+"lost child exit status"; } }
if(in_open) { if(input_off<input.size()) { ssize_t n=write(inpipe[1], input.data()+input_off, input.size()-input_off); if(n>0) input_off+=(size_t)n; else if(n<0 && errno!=EINTR && errno!=EAGAIN && errno!=EWOULDBLOCK) { close(inpipe[1]); in_open=false; } } else { close(inpipe[1]); in_open=false; } }
char buf[4096]; ssize_t n; while((n=read(outpipe[0],buf,sizeof(buf)))>0) r["stdout"] = r["stdout"].to_string()+String(buf,n); if(n==0&&out_open){close(outpipe[0]);out_open=false;}
while((n=read(errpipe[0],buf,sizeof(buf)))>0) r["stderr"] = r["stderr"].to_string()+String(buf,n); if(n==0&&err_open){close(errpipe[0]);err_open=false;}
char buf[4096]; ssize_t n; while((n=read(outpipe[0],buf,sizeof(buf)))>0) { if(output_limit && r["stdout"].to_string().size()+(size_t)n>output_limit) { r["output_limited"].set_bool(true); kill(-pid,SIGKILL); kill(pid,SIGKILL); } else r["stdout"] = r["stdout"].to_string()+String(buf,n); } if(n==0&&out_open){close(outpipe[0]);out_open=false;}
while((n=read(errpipe[0],buf,sizeof(buf)))>0) { if(output_limit && r["stderr"].to_string().size()+(size_t)n>output_limit) { r["output_limited"].set_bool(true); kill(-pid,SIGKILL); kill(pid,SIGKILL); } else r["stderr"] = r["stderr"].to_string()+String(buf,n); } if(n==0&&err_open){close(errpipe[0]);err_open=false;}
if((out_open || err_open || !exited) && wasm_monotonic_ms() >= deadline) { r["timed_out"].set_bool(true); kill(-pid,SIGKILL); kill(pid,SIGKILL); if(!exited) status_valid=waitpid(pid,&status,0)==pid; exited=true; if(in_open){close(inpipe[1]);in_open=false;} if(out_open){close(outpipe[0]);out_open=false;} if(err_open){close(errpipe[0]);err_open=false;} }
if(out_open || err_open || !exited) usleep(10000);
}
@@ -671,8 +673,11 @@ static bool uce_header_name_safe(String name)
return(true);
}
static DValue uce_hardened_http_request_value(const DValue& req, u64 timeout_ms, bool keep_worker_process_group=false);
static DValue uce_http_request_value(const DValue& req)
{
const DValue* security=req.key("security"); if(hardened_http_security_requested(security)) { u64 requested=req.key("timeout_ms")?req.key("timeout_ms")->to_u64(5000):5000; return(uce_hardened_http_request_value(req,std::max<u64>(1,requested))); }
DValue r; r["status"]=(f64)0; r["headers"].set_array(); r["body"]=""; r["error"]="";
const DValue* method_value = req.key("method");
const DValue* url_value = req.key("url");
@@ -706,19 +711,35 @@ static DValue uce_http_request_value(const DValue& req)
return(r);
}
static DValue uce_hardened_http_request_value(const DValue& req, u64 timeout_ms, bool keep_worker_process_group)
{
HardenedHttpHooks hooks;
hooks.resolve=[](String host) { std::vector<String> answers; addrinfo hints{}; hints.ai_socktype=SOCK_STREAM; hints.ai_family=AF_UNSPEC; addrinfo* result=0; if(getaddrinfo(host.c_str(), "443", &hints, &result)!=0) return answers; for(addrinfo* p=result;p;p=p->ai_next) { char text[INET6_ADDRSTRLEN]; if(p->ai_family==AF_INET && inet_ntop(AF_INET,&((sockaddr_in*)p->ai_addr)->sin_addr,text,sizeof(text))) answers.push_back(text); else if(p->ai_family==AF_INET6 && inet_ntop(AF_INET6,&((sockaddr_in6*)p->ai_addr)->sin6_addr,text,sizeof(text))) answers.push_back(text); else answers.push_back(""); } freeaddrinfo(result); return answers; };
hooks.execute=[keep_worker_process_group](std::vector<String> argv, String input, std::vector<String>, u64 deadline, size_t limit) { return hardened_http_exec_argv_capture(argv,input,deadline,limit,true,!keep_worker_process_group); };
return hardened_http_request_internal(req,timeout_ms,hooks);
}
static u64 uce_http_spawn_spec(const DValue& req)
{
uce_job_reap(); u64 id=uce_job_new("http"); if(!id) return(0);
int ready[2]; if(pipe(ready)) { DValue r; r["error"]="pipe failed"; uce_job_finish(id,r,"failed"); return(id); }
pid_t pid=fork();
if(pid==0) { setsid(); uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)getpid())); uce_write_text(uce_job_path(id)+"/state", "running"); DValue result=uce_http_request_value(req); uce_job_finish(id,result,result["error"].to_string()==""?"done":"failed"); _exit(0); }
if(pid<0) { DValue r; r["error"]="fork failed"; uce_job_finish(id,r,"failed"); return(id); }
if(pid==0) { close(ready[0]); if(setsid()<0) _exit(127); char ok='1'; if(write(ready[1],&ok,1)!=1) _exit(127); close(ready[1]); uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)getpid())); uce_write_text(uce_job_path(id)+"/state", "running"); const DValue* security=req.key("security"); bool hardened=hardened_http_security_requested(security); DValue result=hardened ? uce_hardened_http_request_value(req,req.key("timeout_ms")?std::max<u64>(1,req.key("timeout_ms")->to_u64(5000)):5000,true) : uce_http_request_value(req); uce_job_finish(id,result,result["error"].to_string()==""?"done":"failed"); _exit(0); }
close(ready[1]); char ok=0; ssize_t started; do { started=read(ready[0],&ok,1); } while(started<0&&errno==EINTR); close(ready[0]);
if(pid<0 || started!=1 || ok!='1') { DValue r; r["error"]="async worker start failed"; uce_job_finish(id,r,"failed"); return(id); }
uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)pid)); uce_write_text(uce_job_path(id)+"/state", "running"); return(id);
}
static DValue uce_job_status_value(u64 id)
{
DValue r; String dir=uce_job_path(id); r["job_id"]=(f64)id;
if(id==0 || !std::filesystem::is_directory(dir)) { r["state"]="missing"; return(r); }
bool is_directory = false;
if(id != 0)
{
try { is_directory = std::filesystem::is_directory(dir); }
catch(...) { is_directory = false; }
}
if(!is_directory) { r["state"]="missing"; return(r); }
String state=trim(uce_read_text(dir+"/state")); if(state=="") state="pending"; r["state"]=state;
r["kind"]=trim(uce_read_text(dir+"/kind")); r["pid"]=(f64)strtoull(uce_read_text(dir+"/worker_pid").c_str(),0,10);
r["done"].set_bool(state=="done"||state=="failed"||state=="cancelled");
@@ -1868,6 +1889,7 @@ public:
bool writable = false;
};
std::vector<FileHandle> file_handles;
std::vector<int> socket_fds;
struct RequestPerfSnapshot
{
@@ -2025,7 +2047,9 @@ public:
// the wasm-side enforcement of request-scoped DB lifecycle; app code should
// never cache these opaque handles across requests.
std::vector<SQLite*> sqlite_handles;
std::vector<MySQL*> mysql_handles;
std::map<u64, MySQL*> mysql_handles;
std::set<u64> mysql_task_handles;
u64 mysql_next_handle = 1;
std::vector<MySQL*> mysql_request_pool;
std::vector<MySQL*> mysql_request_owned;
#endif
@@ -2042,6 +2066,14 @@ public:
h.fd = -1;
}
}
for(auto& fd : socket_fds)
{
if(fd >= 0)
{
::socket_close((u64)fd);
fd = -1;
}
}
#ifdef UCE_WASM_HOST_CONNECTORS
for(auto* db : sqlite_handles)
if(db)
@@ -3682,15 +3714,20 @@ private:
int32_t input_size = args[1].i32();
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String stage_key = "crypto_operation";
if(!self->hostcall_staged(stage_key, out))
if(input_size > 0 && input_size <= 64 * 1024 && self->hostcall_read(args[0].i32(), input_size, encoded) == "" && ucb_decode(encoded, request, &error))
{
if(input_size > 0 && input_size <= 64 * 1024 && self->hostcall_read(args[0].i32(), input_size, encoded) == "" && ucb_decode(encoded, request, &error))
String stage_key = "crypto_operation:" + encoded;
if(!self->hostcall_staged(stage_key, out))
{
response = crypto_operation_native(request);
else
response["error"] = "invalid_request";
out = ucb_encode(response);
if(buf == 0) self->hostcall_stage(stage_key, out);
}
}
else
{
response["error"] = "invalid_request";
out = ucb_encode(response);
if(buf == 0) self->hostcall_stage(stage_key, out);
}
if(buf && cap >= out.size()) self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
@@ -4122,7 +4159,7 @@ private:
if(mod == "env" && name == "uce_host_file_truncate")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); results[0]=Val((int32_t)(r!=""&&truncate(r.c_str(),(off_t)args[4].i64())==0)); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_dir_remove")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); bool rec=args[4].i32()!=0; bool ok=false; if(r!="") { if(rec) ok=std::filesystem::remove_all(r)>0; else ok=::rmdir(r.c_str())==0; } results[0]=Val((int32_t)ok); return(std::monostate()); }));
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); bool rec=args[4].i32()!=0; bool ok=false; if(r!="") { if(rec) { try { ok=std::filesystem::remove_all(r)>0; } catch(...) { ok=false; } } else ok=::rmdir(r.c_str())==0; } results[0]=Val((int32_t)ok); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_temp")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String prefix,current; self->hostcall_read(args[0].i32(),args[1].i32(),prefix); self->hostcall_read(args[2].i32(),args[3].i32(),current); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); String out; String stage_key="file_temp:"+prefix+"\0"+current; if(!self->hostcall_staged(stage_key,out)) { if(prefix=="") prefix="/tmp/uce-temp"; String templ=self->resolve_guest_write(prefix+"XXXXXX",current); if(templ!="") { std::vector<char> t(templ.begin(), templ.end()); t.push_back(0); int fd=mkstemp(t.data()); if(fd>=0) { close(fd); out=t.data(); } } if(buf==0) self->hostcall_stage(stage_key,out); } if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_chmod")
@@ -4332,9 +4369,15 @@ private:
}));
if(mod == "env" && name == "uce_host_memcache_command")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64();
if(handle < 1 || handle > self->socket_fds.size() || self->socket_fds[(size_t)handle - 1] < 0)
{
results[0] = Val((int32_t)0);
return(std::monostate());
}
String command;
self->hostcall_read(args[1].i32(), args[2].i32(), command);
String key = "memcache:" + std::to_string((u64)args[0].i64()) + ":" + command;
String key = "memcache:" + std::to_string(handle) + ":" + command;
u32 cap = (u32)args[4].i32();
int32_t buf = args[3].i32();
String out;
@@ -4346,7 +4389,7 @@ private:
}
else
{
u64 socket_fd = (u64)args[0].i64();
u64 socket_fd = (u64)self->socket_fds[(size_t)handle - 1];
out = wasm_memcache_exchange(socket_fd, command, self->bounded_hostcall_timeout_ms(1000));
if(buf == 0)
{
@@ -4384,41 +4427,60 @@ private:
String password = request["password"].to_string();
String database = request["database"].to_string();
MySQL* db = 0;
for(auto* pooled : self->mysql_request_pool)
if(pooled && pooled->connection && pooled->request_host == host && pooled->request_username == username && pooled->request_password == password && pooled->request_database == database)
{
db = pooled;
connection_source = "request";
break;
}
bool ok = db != 0;
if(!db)
bool ok = false;
if(task_child_process)
{
bool reused = false;
bool persistent = false;
db = self->worker.mysql_checkout(host, username, password, database, reused, persistent);
connection_source = reused ? "worker" : "new";
ok = db && db->connection;
if(ok && db->connection)
// task() closes inherited descriptors after fork. Never reuse
// request/worker MySQL objects whose sockets were just closed.
db = new MySQL();
ok = db->connect(host, username, password, database);
connection_source = "task";
if(ok)
{
self->mysql_request_pool.push_back(db);
if(!persistent)
self->mysql_request_owned.push_back(db);
self->mysql_request_owned.push_back(db);
}
}
else
{
for(auto* pooled : self->mysql_request_pool)
if(pooled && pooled->connection && pooled->request_host == host && pooled->request_username == username && pooled->request_password == password && pooled->request_database == database)
{
db = pooled;
connection_source = "request";
break;
}
ok = db != 0;
if(!db)
{
bool reused = false;
bool persistent = false;
db = self->worker.mysql_checkout(host, username, password, database, reused, persistent);
connection_source = reused ? "worker" : "new";
ok = db && db->connection;
if(ok)
{
self->mysql_request_pool.push_back(db);
if(!persistent)
self->mysql_request_owned.push_back(db);
}
}
}
u64 handle = 0;
if(ok && db->connection)
if(ok && db->connection && self->mysql_next_handle != 0)
{
db->request_leases++;
self->mysql_handles.push_back(db);
handle = self->mysql_handles.size();
handle = self->mysql_next_handle;
self->mysql_next_handle = handle == UINT64_MAX ? 0 : handle + 1;
self->mysql_handles[handle] = db;
if(task_child_process) self->mysql_task_handles.insert(handle);
if(connection_source == "new") self->mysql_connection_open_count++;
else if(connection_source == "worker") self->mysql_connection_reuse_count++;
else if(connection_source == "request") self->mysql_request_pool_hit_count++;
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->_preload_next_error_code;
response["statement_info"] = db->error();
response["error_code"] = (f64)(handle == 0 ? 2000 : db->_preload_next_error_code);
response["statement_info"] = handle == 0 ? String("mysql handle space exhausted") : db->error();
if(handle == 0 && db)
delete db;
}
@@ -4430,8 +4492,8 @@ private:
else
{
u64 handle = request["handle"].to_u64();
MySQL* db = (handle >= 1 && handle <= self->mysql_handles.size())
? self->mysql_handles[(size_t)handle - 1] : 0;
auto handle_it = self->mysql_handles.find(handle);
MySQL* db = handle_it == self->mysql_handles.end() || (task_child_process && self->mysql_task_handles.find(handle) == self->mysql_task_handles.end()) ? 0 : handle_it->second;
if(op == "query" && db)
{
response["result"] = db->query(request["query"].to_string());
@@ -4444,7 +4506,20 @@ private:
{
if(db->request_leases > 0)
db->request_leases--;
self->mysql_handles[(size_t)handle - 1] = 0;
self->mysql_handles.erase(handle);
self->mysql_task_handles.erase(handle);
if(task_child_process && db->request_leases == 0)
{
auto erase_db = [db](auto& pool) { pool.erase(std::remove(pool.begin(), pool.end(), db), pool.end()); };
erase_db(self->mysql_request_pool);
erase_db(self->mysql_request_owned);
delete db;
}
}
else if(op == "query" || op == "disconnect")
{
response["error_code"] = (f64)2000;
response["statement_info"] = "mysql handle is invalid or unavailable in this task";
}
}
}
@@ -4486,24 +4561,52 @@ private:
String host;
self->hostcall_read(args[0].i32(), args[1].i32(), host);
u64 fd = wasm_socket_connect_bounded(host, (u16)args[2].i32(), self->bounded_hostcall_timeout_ms(self->worker.cfg.invocation_timeout_ms));
results[0] = Val((int64_t)fd);
u64 handle = 0;
if(fd > 0)
{
self->socket_fds.push_back((int)fd);
handle = self->socket_fds.size();
}
results[0] = Val((int64_t)handle);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_socket_close")
return(add([](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
::socket_close((u64)args[0].i64());
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64();
if(handle >= 1 && handle <= self->socket_fds.size())
{
int& fd = self->socket_fds[(size_t)handle - 1];
if(fd >= 0)
{
::socket_close((u64)fd);
fd = -1;
}
}
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_socket_write")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64();
String data;
self->hostcall_read(args[1].i32(), args[2].i32(), data);
results[0] = Val(wasm_socket_write_bounded((u64)args[0].i64(), data, self->bounded_hostcall_timeout_ms(self->worker.cfg.invocation_timeout_ms)) ? (int32_t)1 : (int32_t)0);
bool ok = false;
if(handle >= 1 && handle <= self->socket_fds.size())
{
int fd = self->socket_fds[(size_t)handle - 1];
ok = fd >= 0 && wasm_socket_write_bounded((u64)fd, data, self->bounded_hostcall_timeout_ms(self->worker.cfg.invocation_timeout_ms));
}
results[0] = Val(ok ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_socket_read")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 sockfd = (u64)args[0].i64();
u64 handle = (u64)args[0].i64();
if(handle < 1 || handle > self->socket_fds.size() || self->socket_fds[(size_t)handle - 1] < 0)
{
results[0] = Val((int32_t)0);
return(std::monostate());
}
u64 sockfd = (u64)self->socket_fds[(size_t)handle - 1];
u32 max_length = (u32)args[1].i32();
u32 requested_timeout = (u32)args[2].i32();
u64 requested_ms = requested_timeout == 0 ? self->invocation_remaining_ms() : (u64)requested_timeout * 1000;
@@ -4511,7 +4614,7 @@ private:
int32_t buf = args[3].i32();
u32 cap = (u32)args[4].i32();
// The size and fetch calls share this key, while the remaining budget may change between them.
String key = std::to_string(sockfd) + ":" + std::to_string(max_length) + ":" + std::to_string(requested_timeout);
String key = std::to_string(handle) + ":" + std::to_string(max_length) + ":" + std::to_string(requested_timeout);
String out;
if(buf != 0 && self->staged_socket_read_key == key)
{
@@ -4578,7 +4681,7 @@ private:
// before the hostcall stack unwinds, so `self` points to the child's
// copy of this per-request workspace. The parent request can return and
// destroy its workspace without invalidating the child copy.
u64 task_timeout_ms = timeout > UINT64_MAX / 1000 ? UINT64_MAX : timeout * 1000;
u64 task_timeout_ms = timeout == 0 ? UINT64_MAX : (timeout > UINT64_MAX / 1000 ? UINT64_MAX : timeout * 1000);
auto run_callback = [self, callback_id, task_timeout_ms]() {
String error = self->run_task_callback(callback_id, task_timeout_ms);
if(error != "")