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
+1 -1
View File
@@ -752,7 +752,7 @@ asymmetric operations. The initial allowlist supports `operation=key_generate`
and `operation=jwt_sign` with `algorithm=ES256`. Key generation returns P-256
public/private JWKs plus the RFC 7638 thumbprint (`kid`). JWT signing accepts only
a consistent P-256 private JWK, forces `alg=ES256`, and emits a compact JWT with
a 64-byte JOSE signature. Unknown operations and algorithms fail closed.
a 64-byte JOSE signature. It also supports `cbor_decode`, `cose_es256_parse`, and `es256_verify` with `algorithm=ES256`: inputs are canonical unpadded base64url fields; requests are capped at 32 KiB, while CBOR input is definite-length only and capped at 16 KiB decoded (21,846 base64url characters), 256 nodes, and depth 16; it rejects duplicate keys and trailing bytes, and returns an explicit typed tree (`unsigned`, `negative`, `bytes`, `text`, `array`, `map`) rather than DValue-coerced map keys. COSE accepts only EC2/-7/P-256 with exact 32-byte coordinates and an OpenSSL-valid point. Verification accepts only complete canonical DER ECDSA over `message_base64url` and returns `valid`; malformed material fails closed. V1 has no attestation, Ed25519, or RS256. Unknown operations and algorithms fail closed.
Existing typed digest, HMAC, password, randomness, and constant-time comparison
functions remain separate. `crypto_operation()` exposes no raw signing, arbitrary
+2
View File
@@ -67,6 +67,7 @@ if [[ "$action" == "run" ]]; then
curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "${base_url}&group=${group}"
done
scripts/test_dependency_invalidation.sh
scripts/test_compiler_lock_directory.sh
scripts/test_abi_generation_rollout.sh
scripts/test_parallel_precompile.sh
timeout --signal=TERM --kill-after=5s 175s scripts/test_parallel_proactive_compile.sh
@@ -78,6 +79,7 @@ if [[ "$action" == "run" ]]; then
scripts/test_relative_component_cache.sh
scripts/test_password_hashing.sh
scripts/test_crypto_operation_native.sh
scripts/test_hardened_http_native.sh
scripts/test_mysql_epoch_refresh.sh
scripts/test_mysql_persistent_pool.sh
scripts/test_mysql_persistent_pool_idle.sh
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
if [[ "${1:-}" != "--inside" ]]; then
exec timeout --signal=TERM --kill-after=5s 90s unshare --mount --fork --kill-child=TERM "$0" --inside
fi
root="/tmp/uce-compiler-lock-directory-$$"
site="$root/site"
work="$root/missing/parents/work"
settings="$root/settings.cfg"
log="$root/service.log"
server_pid=""
cleanup() {
if [[ -n "$server_pid" ]] && kill -0 "$server_pid" 2>/dev/null; then
kill -TERM "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
fi
rm -rf "$root"
}
trap cleanup EXIT
mkdir -p "$site" "$root/run" "$root/session" "$root/upload"
printf '%s\n' 'CLI(Request& context) { print("recursive-lock-directory-ok"); }' >"$site/test.uce"
cp /etc/uce/settings.cfg "$settings"
cat >>"$settings" <<CFG
BIN_DIRECTORY=$work
PRECOMPILE_FILES_IN=$site
SITE_DIRECTORY=$site
FCGI_SOCKET_PATH=$root/run/fastcgi.sock
FCGI_PORT=
CLI_SOCKET_PATH=$root/run/cli.sock
WS_BROKER_SOCKET_PATH=$root/run/ws.sock
HTTP_PORT=
HTTP_DOCUMENT_ROOT=$site
SESSION_PATH=$root/session
TMP_UPLOAD_PATH=$root/upload
WASM_CORE_PATH=$(pwd)/bin/wasm/core.wasm
WORKER_COUNT=1
PROACTIVE_COMPILE_ENABLED=0
CFG
mount --bind "$settings" /etc/uce/settings.cfg
bin/uce_fastcgi.linux.bin >"$log" 2>&1 &
server_pid=$!
deadline=$((SECONDS + 20))
while [[ ! -S "$root/run/cli.sock" ]] && (( SECONDS < deadline )); do sleep 0.05; done
[[ -S "$root/run/cli.sock" ]] || { cat "$log" >&2; exit 1; }
rm -rf "$work"
response=$(curl -sS --max-time 45 --fail-with-body --unix-socket "$root/run/cli.sock" http://localhost/test.uce) || { cat "$log" >&2; exit 1; }
[[ "$response" == *"recursive-lock-directory-ok"* ]] || { printf '%s\n' "$response" >&2; cat "$log" >&2; exit 1; }
find "$work" -type f -name '*.lock' -print -quit | grep -q . || { echo "compiler did not recreate a nested lock path" >&2; cat "$log" >&2; exit 1; }
echo "compiler recursive lock directory passed"
+78 -5
View File
@@ -40,6 +40,11 @@ String base64_decode(String raw, bool& ok)
#include "src/lib/hash.cpp"
static String b64url_encode(String text)
{
String out=replace(replace(base64_encode(text),"+","-"),"/","_"); while(!out.empty()&&out.back()=='=') out.pop_back(); return out;
}
static String b64url_decode(String text)
{
text = replace(replace(text, "-", "+"), "_", "/");
@@ -82,22 +87,90 @@ int main()
DValue wrong_curve = key["private_jwk"]; wrong_curve["crv"] = "P-384";
DValue malformed = key["private_jwk"]; malformed["x"] = "bad=";
DValue mismatch = key["private_jwk"]; String d = mismatch["d"].to_string(); d[0] = d[0] == 'A' ? 'B' : 'A'; mismatch["d"] = d;
const String cose="pQECAyYgASFYIGsX0fLhLEJH-Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWIlggT-NC4v4af5uO5-tKfA-eFivOM1drMV7Oy7ZAaDe_UfU", message="d2ViYXV0aG4gZml4ZWQgbWVzc2FnZQ", signature="MEUCIQCkatZK1VVsjk17uvyzyhjdAkMNWXPjxSOMqWcjmM_8XAIgDaSk3Qufyd0_6r9Dm9A8RQbFco-FdTBulq7bvRGoBC4";
auto op = [&](String operation) { DValue r; r["operation"]=operation; r["algorithm"]="ES256"; return r; };
auto parse = [&](String key) { DValue r=op("cose_es256_parse"); r["cose_key_base64url"]=key; return crypto_operation_native(r); };
auto verify_cose = [&](String key,String msg,String sig) { DValue r=op("es256_verify"); r["cose_key_base64url"]=key; r["message_base64url"]=msg; r["signature_der_base64url"]=sig; return crypto_operation_native(r); };
auto decode_cbor = [&](String bytes) { DValue r=op("cbor_decode"); r["cbor_base64url"]=b64url_encode(bytes); return crypto_operation_native(r); };
DValue unsupported; unsupported["operation"] = "encrypt"; unsupported["algorithm"] = "ES256";
DValue unknown_algorithm; unknown_algorithm["operation"] = "key_generate"; unknown_algorithm["algorithm"] = "none";
DValue untyped_algorithm = key_request; untyped_algorithm["algorithm"] = (f64)256;
DValue list_header; list_header.set_array(); DValue list_item; list_item = "not-an-object"; list_header.push(list_item); DValue list_request; list_request["operation"] = "jwt_sign"; list_request["algorithm"] = "ES256"; list_request["private_jwk"] = key["private_jwk"]; list_request["protected_header"] = list_header; list_request["claims"] = claims;
DValue control_request = list_request; control_request["protected_header"] = header; control_request["claims"] = claims; control_request["claims"]["bad"] = String("control\nbyte");
DValue oversized = key_request; oversized["ignored"] = String(17000, 'x');
DValue oversized = key_request; oversized["ignored"] = String(33000, 'x');
DValue nonfinite = key_request; nonfinite["ignored"] = std::numeric_limits<f64>::quiet_NaN();
String tampered = jwt; if(!tampered.empty()) tampered[tampered.size() - 1] = tampered.back() == 'A' ? 'B' : 'A';
String tampered = jwt; if(tampered.size() > 2) tampered[tampered.size() - 2] = tampered[tampered.size() - 2] == 'A' ? 'B' : 'A';
bool kid_ok = key["ok"].to_bool() && key["kid"].to_string() == key["thumbprint"].to_string();
bool signed_ok = signed_result["ok"].to_bool() && jwt != "" && verify(key["public_jwk"], jwt);
bool tamper_ok = !verify(key["public_jwk"], tampered);
auto cbor_error = [&](String bytes) { return(decode_cbor(bytes)["error"].to_string() == "invalid_cbor"); };
auto cose_error = [&](String encoded) { return(parse(encoded)["error"].to_string() == "invalid_cose_key"); };
String cose_raw = b64url_decode(cose);
String wrong_kty = cose_raw; wrong_kty[2] = 1;
String wrong_alg = cose_raw; wrong_alg[4] = 0x27;
String wrong_cose_curve = cose_raw; wrong_cose_curve[6] = 2;
String missing_label = cose_raw; missing_label[0] = 0xa4; missing_label.resize(missing_label.size() - 35);
String short_x = String("\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x1f",10) + String(31, 'x') + String("\x22\x58\x20", 3) + String(32, 'y');
String duplicate_label = cose_raw; duplicate_label[0] = 0xa6; duplicate_label += String("\x01\x02", 2);
String invalid_point = String("\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20", 10) + String(32, 'x') + String("\x22\x58\x20", 3) + String(32, 'y');
String der = b64url_decode(signature);
String tampered_der = der; tampered_der[10] ^= 1;
String trailing_der = der + String("\x00", 1);
String noncanonical_der = der; noncanonical_der[1]++; noncanonical_der[3]++; noncanonical_der.insert(4, 1, '\0');
DValue valid_verify = verify_cose(cose, message, signature);
DValue tampered_signature = verify_cose(cose, message, b64url_encode(tampered_der));
DValue tampered_message = verify_cose(cose, b64url_encode("tampered"), signature);
DValue malformed_der = verify_cose(cose, message, b64url_encode(String("\x30\x00", 2)));
DValue noncanonical_der_result = verify_cose(cose, message, b64url_encode(noncanonical_der));
DValue trailing_der_result = verify_cose(cose, message, b64url_encode(trailing_der));
DValue invalid_point_result = verify_cose(b64url_encode(invalid_point), message, signature);
String large_cbor = String("\x58\x81", 2) + String(129, 'x');
String oversized_cbor = String("\x5a\x00\x00\x40\x00", 5) + String(16384, 'x');
String duplicate_compound = String("\xa2\x82\x41[\x41]\x01\x82\x41[\x41]\x02", 13);
String distinct_compound = String("\xa2\x82\x41[\x41]\x01\x82\x41[\x41[\x02", 13);
String node_overflow = String("\x99\x01\x00", 3) + String(256, 0);
String decoded;
bool cbor_valid = decode_cbor(String("\x82\x01\x62ok", 5))["ok"].to_bool();
bool cbor_large = decode_cbor(large_cbor)["ok"].to_bool();
bool cbor_control_text = decode_cbor(String("\x61\x01", 2))["ok"].to_bool();
bool cbor_duplicate = cbor_error(String("\xa2\x01\x02\x01\x03", 5));
bool cbor_compound_duplicate = cbor_error(duplicate_compound);
bool cbor_compound_distinct = decode_cbor(distinct_compound)["ok"].to_bool();
bool cbor_invalid_utf8 = cbor_error(String("\x61\x80", 2));
bool cbor_truncated = cbor_error(String("\xa1", 1));
bool cbor_trailing = cbor_error(String("\x01\x02", 2));
bool cbor_depth = cbor_error(String(17, '\x81') + "\x00");
bool cbor_nodes = cbor_error(node_overflow);
bool cbor_size = cbor_error(oversized_cbor);
bool cbor_indefinite = cbor_error(String("\x9f\x01\xff", 3));
bool cbor_integer_overflow = cbor_error(String("\x5b\xff\xff\xff\xff\xff\xff\xff\xff", 9));
bool cbor_nonminimal = cbor_error(String("\x18\x17", 2));
bool b64_padding = !uce_base64url_decode(cose + "=", decoded, UCE_CBOR_MAX_BASE64URL);
bool b64_truncated = !uce_base64url_decode("A", decoded, UCE_CBOR_MAX_BASE64URL);
bool b64_trailing_bits = !uce_base64url_decode("AB", decoded, UCE_CBOR_MAX_BASE64URL);
bool cose_valid = parse(cose)["ok"].to_bool();
bool cose_wrong_kty = cose_error(b64url_encode(wrong_kty));
bool cose_wrong_alg = cose_error(b64url_encode(wrong_alg));
bool cose_wrong_curve = cose_error(b64url_encode(wrong_cose_curve));
bool cose_short_coordinate = cose_error(b64url_encode(short_x));
bool cose_missing_label = cose_error(b64url_encode(missing_label));
bool cose_duplicate_label = cose_error(b64url_encode(duplicate_label));
bool cose_invalid_point = invalid_point_result["error"].to_string() == "invalid_key_or_payload";
bool verify_valid = valid_verify["ok"].to_bool() && valid_verify["valid"].to_bool();
bool verify_tampered_signature = tampered_signature["ok"].to_bool() && !tampered_signature["valid"].to_bool();
bool verify_tampered_message = tampered_message["ok"].to_bool() && !tampered_message["valid"].to_bool();
bool verify_malformed_der = malformed_der["error"].to_string() == "invalid_signature";
bool verify_noncanonical_der = noncanonical_der_result["error"].to_string() == "invalid_signature";
bool verify_trailing_der = trailing_der_result["error"].to_string() == "invalid_signature";
bool cbor_ok = cbor_valid && cbor_large && cbor_control_text && cbor_duplicate && cbor_compound_duplicate && cbor_compound_distinct && cbor_invalid_utf8 && cbor_truncated && cbor_trailing && cbor_depth && cbor_nodes && cbor_size && cbor_indefinite && cbor_integer_overflow && cbor_nonminimal;
bool cose_ok = cose_valid && cose_wrong_kty && cose_wrong_alg && cose_wrong_curve && cose_short_coordinate && cose_missing_label && cose_duplicate_label && cose_invalid_point;
bool cose_negative = verify_valid && verify_tampered_signature && verify_tampered_message && verify_malformed_der && verify_noncanonical_der && verify_trailing_der;
bool b64_ok = b64_padding && b64_truncated && b64_trailing_bits;
bool negatives_ok = !sign(wrong_curve)["ok"].to_bool() && !sign(malformed)["ok"].to_bool() && !sign(mismatch)["ok"].to_bool() && crypto_operation_native(unsupported)["error"].to_string() == "unsupported_operation" && crypto_operation_native(unknown_algorithm)["error"].to_string() == "unsupported_algorithm" && crypto_operation_native(untyped_algorithm)["error"].to_string() == "invalid_request" && crypto_operation_native(list_request)["error"].to_string() == "invalid_key_or_payload" && crypto_operation_native(control_request)["error"].to_string() == "invalid_request" && crypto_operation_native(oversized)["error"].to_string() == "invalid_request" && crypto_operation_native(nonfinite)["error"].to_string() == "invalid_request";
if(!(kid_ok && signed_ok && tamper_ok && negatives_ok)) std::cerr << "kid=" << kid_ok << " signed=" << signed_ok << " tamper=" << tamper_ok << " negatives=" << negatives_ok << " jwt_size=" << jwt.size() << "\\n";
return(kid_ok && signed_ok && tamper_ok && negatives_ok ? 0 : 1);
if(!(kid_ok && signed_ok && tamper_ok && negatives_ok && cose_ok && cbor_ok && cose_negative && b64_ok)) std::cerr << "kid=" << kid_ok << " signed=" << signed_ok << " tamper=" << tamper_ok << " negatives=" << negatives_ok << " cbor=" << cbor_ok << " cose=" << cose_ok << " verify=" << cose_negative << " b64=" << b64_ok << " large=" << cbor_large << " compound=" << cbor_compound_duplicate << "/" << cbor_compound_distinct << " der=" << verify_malformed_der << "/" << verify_noncanonical_der << "/" << verify_trailing_der << " cborparts=" << cbor_valid << cbor_control_text << cbor_duplicate << cbor_invalid_utf8 << cbor_truncated << cbor_trailing << cbor_depth << cbor_nodes << cbor_size << cbor_indefinite << cbor_integer_overflow << cbor_nonminimal << "\\n";
return(kid_ok && signed_ok && tamper_ok && negatives_ok && cose_ok && cbor_ok && cose_negative && b64_ok ? 0 : 1);
}
EOF
clang++ -std=c++20 -fpermissive -I. "$test_source" -lpcre2-8 -lcrypto -o "$test_binary"
"${CXX:-c++}" -std=c++20 -fpermissive -I. "$test_source" -lpcre2-8 -lcrypto -o "$test_binary"
"$test_binary"
echo "native structured crypto operation passed"
+37
View File
@@ -0,0 +1,37 @@
#include "src/lib/types.cpp"
#include "src/lib/dvalue.cpp"
#include "src/lib/functionlib.cpp"
#include "src/wasm/hardened_http_internal.h"
#include <iostream>
struct Fake { std::vector<String> answers, argv, env; String input; HardenedHttpExecResult result; };
static DValue request() { DValue r; r["method"]="POST"; r["url"]="https://api.example.test/token"; r["headers"]["Accept"]="application/json"; r["body"]="client_secret=SECRET"; DValue& s=r["security"]; s["https_only"].set_bool(true); s["public_dns_only"].set_bool(true); s["pin_dns"].set_bool(true); s["isolated_curl"].set_bool(true); s["no_redirects"].set_bool(true); return r; }
static DValue run(Fake& f,DValue r) { HardenedHttpHooks h; h.resolve=[&](String){return f.answers;}; h.execute=[&](std::vector<String> a,String i,std::vector<String> e,u64,size_t){f.argv=a;f.input=i;f.env=e;return f.result;}; return hardened_http_request_internal(r,9000,h); }
static String headers(int status=200) { return "HTTP/1.1 "+std::to_string(status)+" OK\r\nContent-Type: application/json\r\n\r\n"; }
static bool exists(String path) { return access(path.c_str(),F_OK)==0; }
static void wait_for(String path) { for(int n=0;n<500&&!exists(path);n++) usleep(1000); }
static bool process_live(pid_t pid) { char state=0; String path="/proc/"+std::to_string((long long)pid)+"/stat"; int fd=open(path.c_str(),O_RDONLY); if(fd<0) return false; char text[256]{}; ssize_t n=read(fd,text,sizeof(text)-1); close(fd); if(n<=0) return false; char* close_paren=strrchr(text,')'); return close_paren&&close_paren[2]!='Z'; }
int main(int argc,char** argv) {
if(argc==3&&String(argv[1])=="--child-timeout") { pid_t child=fork(); if(child==0) { usleep(100000); int fd=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0600); if(fd>=0) { write(fd,"leaked",6); close(fd); } _exit(0); } for(;;) pause(); }
if(argc==2&&String(argv[1])=="--child-output") { String x(4096,'x'); for(int n=0;n<64;n++) write(1,x.data(),x.size()); return 0; }
if(argc==3&&String(argv[1])=="--child-fds") { int inherited=atoi(argv[2]); bool closed=fcntl(inherited,F_GETFD)==-1&&errno==EBADF; bool header=fcntl(3,F_GETFD)!=-1; write(1,closed&&header?"closed":"open",closed&&header?6:4); return closed&&header?0:1; }
if(argc==5&&String(argv[1])=="--child-job") { int ready=open(argv[3],O_WRONLY|O_CREAT|O_TRUNC,0600); if(ready>=0) { write(ready,"ready",5); close(ready); } pid_t child=fork(); if(child==0) { int pidfile=open(argv[4],O_WRONLY|O_CREAT|O_TRUNC,0600); if(pidfile>=0) { String pid=std::to_string((long long)getpid()); write(pidfile,pid.data(),pid.size()); close(pidfile); } usleep(150000); int marker=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0600); if(marker>=0) { write(marker,"leaked",6); close(marker); } _exit(0); } for(;;) pause(); }
if(argc==3&&String(argv[1])=="--child-async-timeout") { setsid(); HardenedHttpExecResult result=hardened_http_exec_argv_capture({"/bin/sleep","1"},"",20,4096,false,false); if(result.timed_out&&result.exit_code==137) { int fd=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0600); if(fd>=0) { write(fd,"typed",5); close(fd); } } return 0; }
if(argc==3&&String(argv[1])=="--child-async-output") { setsid(); HardenedHttpExecResult result=hardened_http_exec_argv_capture({"/proc/self/exe","--child-output"},"",1000,1024,false,false); if(result.output_limited&&result.exit_code==137) { int fd=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0600); if(fd>=0) { write(fd,"typed",5); close(fd); } } return 0; }
bool ok=true; auto need=[&](bool x,const char* n){if(!x){std::cerr<<n<<"\n";ok=false;}};
Fake f; f.answers={"8.8.8.8"}; f.result.exit_code=0; f.result.stderr_text="SECRET"; f.result.headers_text=headers(); f.result.body_text="one\r\n\r\ntwo"; DValue out=run(f,request());
need(out["error"].to_string()==""&&out["error"].to_string().find("SECRET")==String::npos&&out["body"].to_string()=="one\r\n\r\ntwo","body framing / non-secret errors"); need(f.argv.size()>2&&f.argv[0]=="/usr/bin/curl"&&f.argv[1]=="--disable","absolute curl disable"); need(f.env.size()==1&&f.env[0]=="PATH=/usr/bin:/bin","clean env"); need(std::find(f.argv.begin(),f.argv.end(),"SECRET")==f.argv.end()&&f.input.find("SECRET")!=String::npos,"secret stdin only"); need(std::find(f.argv.begin(),f.argv.end(),"--resolve")!=f.argv.end(),"pinned dns");
Fake interim; interim.answers={"8.8.8.8"}; interim.result.exit_code=0; interim.result.headers_text="HTTP/1.1 100 Continue\r\n\r\n"+headers(201); interim.result.body_text="{}"; need(run(interim,request())["status"].to_u64()==201,"interim headers");
for(String bad:{"127.0.0.1","10.0.0.1","169.254.1.1","192.168.1.1","::1","2001:db8::1"}) { Fake x; x.answers={"8.8.8.8",bad}; need(run(x,request())["error"].to_string()=="unsafe_dns","dns matrix"); }
for(auto pair:std::vector<std::pair<int,String>>{{302,headers(302)},{500,headers(500)},{200,"bad\r\n\r\n"}}) { Fake x; x.answers={"8.8.8.8"};x.result.exit_code=0;x.result.headers_text=pair.second;need(run(x,request())["error"].to_string()==(pair.first==302?"redirect_not_allowed":pair.first==500?"http_status":"malformed_output"),"status errors"); }
DValue bad=request(); bad["method"]="TRACE"; Fake x; need(run(x,bad)["error"].to_string()=="invalid_request"&&x.argv.empty(),"method validation"); bad=request();bad["headers"]["Host"]="evil";need(run(x,bad)["error"].to_string()=="invalid_request","header validation"); bad=request();bad["headers"]["Accept"]="ok\r\nInjected: x";need(run(x,bad)["error"].to_string()=="invalid_request","header CRLF validation");bad=request();bad["url"]="https://127.0.0.1/";need(run(x,bad)["error"].to_string()=="invalid_request","url validation");bad=request();bad["follow_redirects"].set_bool(true);need(run(x,bad)["error"].to_string()=="invalid_request","redirect composition");
for(String key:{"https_only","public_dns_only","pin_dns","isolated_curl","no_redirects"}) { bad=request(); bad["security"].remove(key); need(run(x,bad)["error"].to_string()=="invalid_request","partial hardening fails closed"); bad=request(); bad["security"][key].set_bool(false); need(run(x,bad)["error"].to_string()=="invalid_request","false hardening fails closed"); bad=request(); bad["security"][key]="true"; need(run(x,bad)["error"].to_string()=="invalid_request","non-boolean hardening fails closed"); }
DValue legacy=request(); legacy["security"].clear(); legacy["security"]["unrelated"]="value"; need(!hardened_http_security_requested(legacy.key("security")),"unknown security object remains legacy");
Fake large;large.answers={"8.8.8.8"};large.result.exit_code=0;large.result.headers_text=headers();large.result.body_text=String(65537,'x');need(run(large,request())["error"].to_string()=="response_too_large","body cap");
String base="/tmp/hardened-http-"+std::to_string((long long)getpid()), marker=base+"-marker"; unlink(marker.c_str()); HardenedHttpExecResult timeout=hardened_http_exec_argv_capture({"/proc/self/exe","--child-timeout",marker},"",30,4096,false); usleep(150000); need(timeout.timed_out&&timeout.exit_code==137&&!exists(marker),"timeout kills descendants"); unlink(marker.c_str());
HardenedHttpExecResult overflow=hardened_http_exec_argv_capture({"/proc/self/exe","--child-output"},"",1000,1024,false);need(overflow.output_limited&&overflow.body_text.size()<=1024,"output cap/reap");
int inherited=open("/dev/null",O_RDONLY), high_inherited=fcntl(inherited,F_DUPFD,10); close(inherited); HardenedHttpExecResult fds=hardened_http_exec_argv_capture({"/proc/self/exe","--child-fds",std::to_string(high_inherited)},"",1000,4096,false); close(high_inherited); need(fds.exit_code==0&&fds.body_text=="closed","only stdio and header fd inherited");
String ready=base+"-ready", descendant=base+"-descendant", sentinel=base+"-sentinel"; unlink(marker.c_str()); unlink(ready.c_str()); unlink(descendant.c_str()); unlink(sentinel.c_str()); pid_t worker=fork(); if(worker==0) { setsid(); hardened_http_exec_argv_capture({"/proc/self/exe","--child-job",marker,ready,descendant},"",5000,4096,false,false); _exit(0); } wait_for(ready); wait_for(descendant); pid_t unrelated=fork(); if(unrelated==0) { setsid(); usleep(150000); int fd=open(sentinel.c_str(),O_WRONLY|O_CREAT|O_TRUNC,0600); if(fd>=0) { write(fd,"alive",5); close(fd); } _exit(0); } pid_t descendant_pid=exists(descendant)?(pid_t)strtol([](String path){ int fd=open(path.c_str(),O_RDONLY); char text[32]{}; ssize_t n=fd<0?-1:read(fd,text,sizeof(text)-1); if(fd>=0) close(fd); return String(text,n>0?(size_t)n:0); }(descendant).c_str(),0,10):0; need(exists(ready)&&descendant_pid>0,"async worker and descendant started"); kill(-worker,SIGKILL); waitpid(worker,0,0); usleep(250000); waitpid(unrelated,0,0); need(!process_live(descendant_pid)&&!exists(marker)&&exists(sentinel),"async cancellation leaves no live descendant and kills only its worker group"); unlink(marker.c_str()); unlink(ready.c_str()); unlink(descendant.c_str()); unlink(sentinel.c_str());
String async_timeout=base+"-async-timeout"; unlink(async_timeout.c_str()); pid_t timeout_worker=fork(); if(timeout_worker==0) { execl("/proc/self/exe","test_hardened_http_native","--child-async-timeout",async_timeout.c_str(),(char*)0); _exit(127); } int timeout_status=0; waitpid(timeout_worker,&timeout_status,0); need(WIFEXITED(timeout_status)&&WEXITSTATUS(timeout_status)==0&&exists(async_timeout),"async timeout records a typed terminal result before worker exit"); unlink(async_timeout.c_str());
String async_output=base+"-async-output"; unlink(async_output.c_str()); pid_t output_worker=fork(); if(output_worker==0) { execl("/proc/self/exe","test_hardened_http_native","--child-async-output",async_output.c_str(),(char*)0); _exit(127); } int output_status=0; waitpid(output_worker,&output_status,0); need(WIFEXITED(output_status)&&WEXITSTATUS(output_status)==0&&exists(async_output),"async output cap records a typed terminal result before worker exit"); unlink(async_output.c_str());
return ok?0:1;
}
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
repo=$(cd "$(dirname "$0")/.." && pwd)
bin="${TMPDIR:-/tmp}/test_hardened_http_native.$$"
trap 'rm -f "$bin"' EXIT
compiler=$(command -v clang++ || command -v g++)
"$compiler" -std=c++20 -fpermissive -I"$repo" "$repo/scripts/test_hardened_http_native.cpp" -lpcre2-8 -o "$bin"
timeout --signal=TERM --kill-after=2s 15s "$bin"
+1 -1
View File
@@ -8,7 +8,7 @@ return value : map with ok, bounded error code, and operation-specific output
:content
Runs one explicitly supported structured asymmetric cryptographic operation. The initial allowlist is `key_generate` with `ES256` and `jwt_sign` with `ES256`. Unknown operations and algorithms fail closed.
ES256 signing validates that `x`, `y`, and `d` form one P-256 key, forces the protected `alg` to `ES256`, and emits a compact JWT with a 64-byte JOSE signature. Requests, nesting, values, and output are bounded. Header and claims roots must be JSON objects containing valid UTF-8 without raw control bytes.
ES256 signing validates that `x`, `y`, and `d` form one P-256 key, forces the protected `alg` to `ES256`, and emits a compact JWT with a 64-byte JOSE signature. Requests are capped at 32 KiB; `cbor_decode` accepts at most 16 KiB decoded CBOR (21,846 canonical base64url characters), 256 nodes, and depth 16. CBOR is definite-length only, validates UTF-8 text, preserves typed map keys, rejects structurally duplicate keys and trailing bytes, and reports malformed CBOR as `invalid_cbor`. Header and claims roots must be JSON objects containing valid UTF-8 without raw control bytes.
This function does not replace typed digest, HMAC, password, randomness, or constant-time comparison APIs. It exposes no raw signing, arbitrary curve/digest selection, encryption, or generic OpenSSL access. Keep returned private JWKs secret.
+27 -2
View File
@@ -1,8 +1,18 @@
# http_request
Performs a bounded outbound HTTP(S) request using the runtime `curl` binary. Request fields: `method`, `url`, `headers`, `body`, `timeout_ms`, `follow_redirects`.
Performs an outbound HTTP(S) request using the runtime `curl` binary. Existing request fields remain backward-compatible: `method`, `url`, `headers`, `body`, `timeout_ms`, and `follow_redirects`.
Returns `{ status, headers, body, error }`. `headers` is a name/value map. A missing `curl` binary returns a clear `error` string.
An absent `security` map, or a map containing none of these recognized keys, keeps the legacy request behavior. If a `security` map contains any recognized key, it selects hardening and **every** recognized field below must be explicitly boolean `true`; missing, partial, non-true, or `false` fields fail closed with `invalid_request`.
- `https_only` rejects non-HTTPS URLs, IP-literal hosts, and URL userinfo.
- `public_dns_only` validates **every** DNS answer against the public IPv4 policy. IPv6 answers currently fail closed.
- `pin_dns` pins curl to one validated answer with `--resolve`, retaining the URL hostname for TLS/SNI.
- `isolated_curl` uses absolute `/usr/bin/curl`, `--disable` as argv[1], cleared environment, no proxy/config/netrc/HSTS/Alt-Svc inheritance, no redirects, a three-second connect bound, and a ten-second total bound.
- `no_redirects` makes redirect following and `follow_redirects=true` invalid composition.
Hardened requests bound body input/output to 64 KiB and response headers to 8 KiB. Async hardened requests keep curl and its descendants in the job worker process group, so cancelling that job kills that group only. Methods are limited to `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS`; header names/values are validated and caller-controlled `Host`, framing, connection, and expectation headers are rejected. Sensitive request bodies go to curl stdin, never argv. Errors are typed non-secret values such as `invalid_request`, `unsafe_dns`, `timeout`, `response_too_large`, `redirect_not_allowed`, `http_status`, and `network_failure`.
Returns `{ status, headers, body, error }`.
:see
>socket
@@ -12,3 +22,18 @@ DValue req; req["method"] = "GET"; req["url"] = "http://127.0.0.1/doc/index.uce"
req["headers"]["Host"] = "uce.openfu.com"; req["timeout_ms"] = (f64)2000;
DValue resp = http_request(req);
print("HTTP ", resp["status"].to_u64(), ", ", resp["body"].to_string().length(), " bytes returned\n");
:example
// GitHub token exchange: client_secret stays in stdin body, not argv.
DValue token; token["method"]="POST"; token["url"]="https://github.com/login/oauth/access_token";
token["headers"]["Accept"]="application/json"; token["headers"]["Content-Type"]="application/x-www-form-urlencoded";
token["body"]="client_id="+uri_encode(client_id)+"&client_secret="+uri_encode(client_secret)+"&code="+uri_encode(code);
for(String key:{"https_only","public_dns_only","pin_dns","isolated_curl","no_redirects"}) token["security"][key].set_bool(true);
DValue token_response=http_request(token);
:example
// ATProto metadata discovery uses the same generic policy; no provider operation name.
DValue meta; meta["method"]="GET"; meta["url"]="https://"+issuer_host+"/.well-known/oauth-authorization-server";
meta["headers"]["Accept"]="application/json";
for(String key:{"https_only","public_dns_only","pin_dns","isolated_curl","no_redirects"}) meta["security"][key].set_bool(true);
DValue metadata=http_request(meta);
+1 -1
View File
@@ -1,6 +1,6 @@
# http_request_async
Starts the same bounded curl-backed request as `http_request()` in the file-backed async job registry and returns a job id. Use `job_await()` or `job_result()` to retrieve the HTTP result.
Starts the same request shape as `http_request()`, including its opt-in `security` hardening fields, in the file-backed async job registry and returns a job id. An unknown `security` object remains legacy; any recognized hardening key requires all five keys to be explicit `true` or the request fails closed. Hardened curl remains in this job's worker process group, so `job_cancel()` kills curl and its descendants without signalling other jobs. Use `job_await()` or `job_result()` to retrieve the HTTP result.
:see
>socket
+4 -1
View File
@@ -16,6 +16,8 @@ Executes a MySQL query and returns the resulting data, if any.
`params` provides the query parameter values used by the statement. Use named `:name` placeholders only; positional `?` placeholders are rejected.
Ordinary placeholders are escaped and quoted as SQL string values. For grammar positions that require an unquoted non-negative integer, such as `LIMIT` and `OFFSET`, append `!` to the placeholder (`:limit!`). Unsigned placeholders fail before query execution unless their value is a non-empty sequence of decimal digits. They never accept signs, whitespace, expressions, identifiers, or other SQL fragments.
The result is returned as a `DValue`, which makes it easy to iterate through rows and read fields with the usual `DValue` accessors.
After an insert, update, or delete, use `mysql_affected_rows()` to inspect how many rows changed.
@@ -24,7 +26,8 @@ After an insert, update, or delete, use `mysql_affected_rows()` to inspect how m
MySQL* db = mysql_connect();
if(db != 0)
{
DValue rows = mysql_query(db, "select 'ada@example.test' as email, 1 + 1 as total");
StringMap params; params["limit"] = "1";
DValue rows = mysql_query(db, "select 'ada@example.test' as email, 1 + 1 as total limit :limit!", params);
String email = "none"; String total = "?";
rows.each([&](DValue r, String key) { email = r["email"].to_string(); total = r["total"].to_string(); });
print(email, " / total=", total, "\n");
+1 -1
View File
@@ -21,7 +21,7 @@ Starts a repeating background worker process.
If a process with the same `key` is already running anywhere in the runtime instance, `task_repeat()` does not start a second worker and instead returns the PID of the existing one. Coordination is through the same shared task state used by `task()`.
`timeout` bounds the lifetime of the repeating worker. The default is ten minutes. Pass `0` only for workers that have another shutdown path.
`timeout` bounds the lifetime of the repeating worker. The default is ten minutes. Pass `0` only for workers that have another shutdown path. In Wasm, each individual callback invocation remains bounded by the runtime invocation timeout even when the repeating process lifetime is unbounded.
:example
task_repeat("doc-demo-repeat", 60.0, []() { usleep(10000); });
+31
View File
@@ -20,6 +20,37 @@ CLI(Request& context)
print(cli_arg(context, "missing", "fallback"), "\n");
return;
}
if(action == "task_repeat_unbounded")
{
String marker = "/tmp/uce-task-repeat-unbounded.txt";
file_unlink(marker);
pid_t pid = task_repeat("uce-task-repeat-unbounded", 0.05, [marker]() { file_put_contents(marker, "ran"); }, 0);
usleep(200000);
bool ran = file_get_contents(marker) == "ran";
if(pid > 0) task_kill(pid, 15);
file_unlink(marker);
print(pid > 0 && ran ? "task repeat unbounded ok\n" : "task repeat unbounded failed\n");
return;
}
if(action == "mysql_unsigned_params")
{
MySQL db;
StringMap params; params["limit"] = "1";
String parsed = db.parse_query_parameters("SELECT 7 LIMIT :limit!", params);
StringMap comparison; comparison["left"] = "7"; comparison["right"] = "8";
String inequality = db.parse_query_parameters("SELECT :left!=:right", comparison);
StringMap empty_name; empty_name[""] = "1";
db.parameter_error = false; db.statement_info = "";
db.parse_query_parameters("SELECT 1 LIMIT :!", empty_name);
bool empty_rejected = db.parameter_error;
StringMap invalid; invalid["limit"] = "1;SELECT 9";
db.parameter_error = false;
db.statement_info = "";
db.parse_query_parameters("SELECT 1 LIMIT :limit!", invalid);
bool ok = parsed.find("LIMIT 1") != String::npos && inequality.find("'7'!='8'") != String::npos && empty_rejected && db.parameter_error && db.statement_info.find("must contain only decimal digits") != String::npos;
print(ok ? "mysql unsigned params ok\n" : "mysql unsigned params failed: parsed=" + parsed + ", parameter_error=" + (db.parameter_error ? "1" : "0") + ", statement=" + db.statement_info + "\n");
return;
}
context.set_status(400, "CLI Error");
print("unknown cli action: ", action, "\n");
}
+11 -3
View File
@@ -31,7 +31,7 @@ RENDER(Request& context)
StringMap colon_uri_headers = split_http_headers("GET /clock.uce?t=12:30 HTTP/1.1\r\nHost: colon.example\r\n");
check("split_http_headers() request line with colon in URI", colon_uri_headers["REQUEST_METHOD"] == "GET" && colon_uri_headers["DOCUMENT_URI"] == "/clock.uce" && colon_uri_headers["QUERY_STRING"] == "t=12:30" && colon_uri_headers["HTTP_HOST"] == "colon.example", var_dump(colon_uri_headers));
check("trim() / split_kv() / split_http_headers()", trim(" padded value ") == "padded value" && kv["alpha"] == "one" && kv["empty"] == "" && http_headers["REQUEST_METHOD"] == "GET" && http_headers["DOCUMENT_URI"] == "/demo.uce" && http_headers["QUERY_STRING"] == "x=1" && http_headers["HTTP_X_EMPTY"] == "" && leading_crlf_headers["REQUEST_METHOD"] == "GET" && leading_crlf_headers["DOCUMENT_URI"] == "/lead.uce" && leading_crlf_headers["HTTP_HOST"] == "lead.example" && header_only["REQUEST_METHOD"] == "" && header_only["HTTP_HOST"] == "example.test" && header_only["HTTP_X_TOKEN"] == "abc", trim(" padded value ") + " / " + var_dump(kv) + " / " + var_dump(http_headers) + " / " + var_dump(leading_crlf_headers) + " / " + var_dump(header_only));
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done" && replace("hello", "", "X") == "hello", replace("alpha-beta-beta", "beta", "done"));
check("html_escape() attribute-safe quotes", html_escape("<&>\"Don't") == "&lt;&amp;&gt;&quot;Don&#39;t", html_escape("<&>\"Don't"));
check("regex_match()", regex_match("[A-Z][a-z]+", "Alice") && !regex_match("[A-Z][a-z]+", "Alice!"), "full-string validation");
@@ -212,11 +212,19 @@ RENDER(Request& context)
String decoded_base64 = base64_decode(encoded_base64, base64_ok);
bool invalid_base64_ok = true;
base64_decode("AA=A", invalid_base64_ok);
check("base64_encode() / base64_decode() binary-safe", base64_ok && decoded_base64 == binary_payload && decoded_base64.size() == binary_payload.size() && !invalid_base64_ok, encoded_base64 + " bytes=" + std::to_string((u64)decoded_base64.size()));
bool early_padding_base64_ok = true;
base64_decode("A=AA", early_padding_base64_ok);
check("base64_encode() / base64_decode() binary-safe", base64_ok && decoded_base64 == binary_payload && decoded_base64.size() == binary_payload.size() && !invalid_base64_ok && !early_padding_base64_ok, encoded_base64 + " bytes=" + std::to_string((u64)decoded_base64.size()));
String utf8_sample = "A\xC3\xA9";
auto utf8_parts = split_utf8(utf8_sample);
check("split_utf8()", utf8_parts.size() == 2, "count=" + std::to_string(utf8_parts.size()));
String truncated_utf8 = "abc";
truncated_utf8.push_back((char)0xE2);
truncated_utf8.push_back((char)0x82);
auto truncated_utf8_parts = split_utf8(truncated_utf8);
String lone_utf8_lead(1, (char)0xF0);
auto compound_lone_utf8_parts = split_utf8(lone_utf8_lead, true);
check("split_utf8()", utf8_parts.size() == 2 && truncated_utf8_parts.size() == 4 && truncated_utf8_parts[3].size() == 2 && compound_lone_utf8_parts.size() == 1 && compound_lone_utf8_parts[0] == lone_utf8_lead, "count=" + std::to_string(utf8_parts.size()) + " truncated=" + std::to_string(truncated_utf8_parts.size()));
DValue payload;
payload["name"] = "uce";
+17 -3
View File
@@ -41,6 +41,17 @@ RENDER(Request& context)
wrong_request["private_jwk"] = wrong_curve;
DValue malformed_request = sign_request;
malformed_request["private_jwk"] = malformed;
DValue cose_request;
cose_request["operation"] = "cose_es256_parse";
cose_request["algorithm"] = "ES256";
cose_request["cose_key_base64url"] = "pQECAyYgASFYIGsX0fLhLEJH-Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWIlggT-NC4v4af5uO5-tKfA-eFivOM1drMV7Oy7ZAaDe_UfU";
DValue cose = crypto_operation(cose_request);
DValue verify_request;
verify_request["operation"] = "es256_verify";
verify_request["algorithm"] = "ES256";
verify_request["cose_key_base64url"] = cose_request["cose_key_base64url"];
verify_request["message_base64url"] = "d2ViYXV0aG4gZml4ZWQgbWVzc2FnZQ";
verify_request["signature_der_base64url"] = "MEUCIQCkatZK1VVsjk17uvyzyhjdAkMNWXPjxSOMqWcjmM_8XAIgDaSk3Qufyd0_6r9Dm9A8RQbFco-FdTBulq7bvRGoBC4";
DValue unsupported;
unsupported["operation"] = "encrypt";
unsupported["algorithm"] = "ES256";
@@ -53,9 +64,12 @@ RENDER(Request& context)
list_request["protected_header"] = list_header;
DValue control_request = sign_request;
control_request["claims"]["bad"] = String("control\nbyte");
DValue oversized = key_request;
oversized["ignored"] = String(17000, 'x');
check("crypto_operation() ES256 key generation and JWT signing", key["ok"].to_bool() && signed_result["ok"].to_bool() && key["private_jwk"]["kty"].to_string() == "EC" && key["private_jwk"]["crv"].to_string() == "P-256" && key["public_jwk"]["d"].to_string() == "" && key["kid"].to_string() == key["thumbprint"].to_string() && parts.size() == 3 && parts[2].size() == 86 && decoded_header.find("ES256") != String::npos && !crypto_operation(wrong_request)["ok"].to_bool() && !crypto_operation(malformed_request)["ok"].to_bool() && crypto_operation(unsupported)["error"].to_string() == "unsupported_operation" && !crypto_operation(list_request)["ok"].to_bool() && crypto_operation(control_request)["error"].to_string() == "invalid_request" && crypto_operation(oversized)["error"].to_string() == "invalid_request", key["kid"].to_string());
check("crypto_operation() generates an ES256 key", key["ok"].to_bool() && key["private_jwk"]["kty"].to_string() == "EC" && key["private_jwk"]["crv"].to_string() == "P-256" && key["public_jwk"]["d"].to_string() == "" && key["kid"].to_string() == key["thumbprint"].to_string(), key["kid"].to_string());
check("crypto_operation() signs ES256 JWTs", signed_result["ok"].to_bool() && parts.size() == 3 && parts[2].size() == 86 && decoded_header.find("ES256") != String::npos, signed_result["error"].to_string());
check("crypto_operation() rejects invalid signing requests", !crypto_operation(wrong_request)["ok"].to_bool() && !crypto_operation(malformed_request)["ok"].to_bool() && !crypto_operation(list_request)["ok"].to_bool() && crypto_operation(control_request)["error"].to_string() == "invalid_request", "signing validation");
check("crypto_operation() parses an ES256 COSE key", cose["ok"].to_bool(), cose["error"].to_string());
check("crypto_operation() verifies ES256 DER signatures", crypto_operation(verify_request)["ok"].to_bool() && crypto_operation(verify_request)["valid"].to_bool(), crypto_operation(verify_request)["error"].to_string());
check("crypto_operation() rejects unsupported operations", crypto_operation(unsupported)["error"].to_string() == "unsupported_operation", crypto_operation(unsupported)["error"].to_string());
site_tests_summary(passed, failed, skipped, "Structured crypto tests generate ephemeral P-256 keys and retain no key material.");
site_tests_page_end();
}
+4
View File
@@ -31,6 +31,8 @@ RENDER(Request& context)
};
String nested = preprocessor_nested_literal();
String trailing_backslash = "C:\\Users\\";
String escaped_quote = "before\"after";
site_tests_page_start("Preprocessor", "Regression coverage for literal output rewriting and parser edge cases.");
?>
@@ -43,6 +45,8 @@ RENDER(Request& context)
check("raw string terminator in nested literal", contains(nested, "nested )\" marker"), nested);
check("entrypoint @fragment attribute captures output", context.call["fragments"]["preprocessor-test"].to_string() == "fragment attr once", context.call["fragments"]["preprocessor-test"].to_string());
check("inline code island after dangerous literal", true, "parser returned to C++ after rendering literal content containing )\"");
check("quote scanner handles trailing escaped backslash", trailing_backslash == "C:\\Users\\", trailing_backslash);
check("quote scanner retains escaped quote content", escaped_quote == "before\"after", escaped_quote);
site_tests_summary(passed, failed, skipped, "Literal content containing the C++ raw-string terminator sequence must compile and render unchanged.");
site_tests_page_end();
+26 -3
View File
@@ -28,14 +28,20 @@ RENDER(Request& context)
u64 sockfd = socket_connect("127.0.0.1", 80);
if(sockfd != 0)
{
u64 closed_handle = socket_connect("127.0.0.1", 80);
bool opaque_handles = sockfd == 1 && closed_handle == 2;
socket_close(closed_handle);
socket_close(closed_handle);
socket_close(999999);
bool closed_write_rejected = !socket_write(closed_handle, "invalid");
bool write_ok = socket_write(sockfd, "GET /tests/index.uce HTTP/1.0\r\nHost: uce.openfu.com\r\n\r\n");
String response = socket_read(sockfd, 4096, 2);
socket_close(sockfd);
bool has_nul = response.find(String("\0", 1)) != String::npos;
mark(
"socket_connect() / socket_write() / socket_read()",
(write_ok && response.find("200 OK") != String::npos && !has_nul) ? "pass" : "fail",
response.substr(0, response.length() > 220 ? 220 : response.length()) + (has_nul ? " [unexpected NUL]" : "")
"socket handles reject invalid and closed indices",
(opaque_handles && closed_write_rejected && write_ok && response.find("200 OK") != String::npos && !has_nul) ? "pass" : "fail",
"handles=" + std::to_string(sockfd) + "/" + std::to_string(closed_handle) + " " + response.substr(0, response.length() > 180 ? 180 : response.length()) + (has_nul ? " [unexpected NUL]" : "")
);
}
else
@@ -130,6 +136,15 @@ RENDER(Request& context)
parsed_underscore.find("'Ada'") != String::npos && parsed_underscore.find("_name") == String::npos ? "pass" : "fail",
parsed_underscore
);
StringMap quoted_params;
quoted_params["name_looking_text"] = "SUBSTITUTED";
String escaped_quote_query = "SELECT 'a\\':name_looking_text'";
String parsed_escaped_quote = placeholder_guard.parse_query_parameters(escaped_quote_query, quoted_params);
mark(
"mysql named placeholders stay opaque after escaped quotes",
parsed_escaped_quote.find(":name_looking_text") != String::npos && parsed_escaped_quote.find("SUBSTITUTED") == String::npos ? "pass" : "fail",
parsed_escaped_quote
);
MySQL unavailable_mysql;
unavailable_mysql.connect("127.0.0.1", "__uce_intentionally_missing__", "not-a-real-password");
mark(
@@ -173,6 +188,14 @@ RENDER(Request& context)
inserted == 2 && updated == 1 && selected == 0 ? "pass" : "fail",
"inserted=" + std::to_string((u64)inserted) + " updated=" + std::to_string((u64)updated) + " selected=" + std::to_string((u64)selected)
);
mysql.query("INSERT INTO uce_affected_rows_test VALUES (1,30)");
u32 duplicate_error_code = mysql._preload_next_error_code;
String duplicate_error = mysql.error();
mark(
"mysql query failures retain server diagnostics",
duplicate_error_code > 1 && duplicate_error != "" && duplicate_error != "Unknown server error" ? "pass" : "fail",
"code=" + std::to_string((u64)duplicate_error_code) + " error=" + duplicate_error
);
}
DValue mysql_perf = request_perf();
String mysql_operations = json_encode(mysql_perf["mysql_operations"]);
+5
View File
@@ -18,6 +18,10 @@ RENDER(Request& context)
auto unit_paths = units_list();
DValue info = unit_info("call_helpers.uce");
DValue relative_info = unit_info("components/../relative-child.uce");
String outside_unit = "/tmp/uce-site-tests-outside-unit.uce";
file_put_contents(outside_unit, "RENDER(Request& context) {}");
DValue outside_info = unit_info(outside_unit);
file_unlink(outside_unit);
ob_start();
unit_call("call_helpers.uce", "emit_marker");
@@ -32,6 +36,7 @@ RENDER(Request& context)
check("units_list()", unit_paths.size() > 0, "count=" + std::to_string(unit_paths.size()));
check("unit_info()", info["path"].to_string() != "", json_encode(info));
check("compiler canonicalizes relative unit paths", relative_info["path"].to_string().find("/../") == String::npos && str_ends_with(relative_info["path"].to_string(), "/site/tests/relative-child.uce"), json_encode(relative_info));
check("compiler rejects units outside document root", outside_info["path"].to_string() == "", json_encode(outside_info));
check("unit_compile()", unit_compile("call_helpers.uce"), "call_helpers.uce");
check("unit_call()", call_output.find("UNIT_CALL_EXPORT_OK") != String::npos, call_output);
check("unit_render()", render_output.find("data-unit-render=\"ok\"") != String::npos, render_output);
+8 -2
View File
@@ -74,8 +74,14 @@ void compiler_code_state_consume(CompilerCodeState& state, String& buffer, const
if(state.inside_quote)
{
if(state.quote_char == c && (i == 0 || content[i-1] != '\\'))
state.inside_quote = false;
if(state.quote_char == c)
{
u32 backslashes = 0;
for(u32 j = i; j > 0 && content[j - 1] == '\\'; j -= 1)
backslashes += 1;
if(backslashes % 2 == 0)
state.inside_quote = false;
}
return;
}
+32 -6
View File
@@ -646,7 +646,10 @@ int compiler_open_lock_file(String file_name, String purpose, bool nonblocking =
(void)purpose;
auto lock_dir = dirname(file_name);
if(lock_dir != "")
mkdir(lock_dir);
{
std::error_code error;
std::filesystem::create_directories(lock_dir, error);
}
int fdlock = open(file_name.c_str(), O_RDWR | O_CREAT, 0666);
if(fdlock == -1 && (errno == EACCES || errno == EPERM))
fdlock = open(file_name.c_str(), O_RDONLY | O_CLOEXEC);
@@ -676,7 +679,10 @@ int compiler_open_lock_file_bounded(String file_name, String purpose, CompilerDe
return(compiler_open_lock_file(file_name, purpose));
auto lock_dir = dirname(file_name);
if(lock_dir != "")
mkdir(lock_dir);
{
std::error_code error;
std::filesystem::create_directories(lock_dir, error);
}
int fdlock = open(file_name.c_str(), O_RDWR | O_CREAT, 0666);
if(fdlock == -1 && (errno == EACCES || errno == EPERM))
fdlock = open(file_name.c_str(), O_RDONLY | O_CLOEXEC);
@@ -734,14 +740,34 @@ static void compiler_mark_source_generation_nonblocking(Request* context)
String compiler_normalize_unit_path(Request* context, String file_name)
{
file_name = trim(file_name);
if(file_name == "")
if(file_name == "" || !context || !context->server)
return("");
if(file_name[0] != '/')
file_name = expand_path(file_name, context->server->config["COMPILER_SYS_PATH"]);
String canonical = path_real(file_name);
if(canonical != "")
return(canonical);
return(file_name);
if(canonical == "")
{
std::error_code error;
canonical = std::filesystem::weakly_canonical(file_name, error).string();
if(error || canonical == "")
return("");
}
String allowed_root = trim(first(
context->params["DOCUMENT_ROOT"],
context->server->config["PRECOMPILE_FILES_IN"],
context->server->config["HTTP_DOCUMENT_ROOT"],
path_join(context->server->config["COMPILER_SYS_PATH"], context->server->config["SITE_DIRECTORY"])
));
if(allowed_root == "")
return("");
if(allowed_root[0] != '/')
allowed_root = expand_path(allowed_root, context->server->config["COMPILER_SYS_PATH"]);
allowed_root = path_real(allowed_root);
if(allowed_root == "")
return("");
if(allowed_root[allowed_root.length() - 1] != '/')
allowed_root += "/";
return(canonical + "/" == allowed_root || str_starts_with(canonical, allowed_root) ? canonical : "");
}
bool compiler_is_known_unit_file(String file_name)
+8 -6
View File
@@ -140,6 +140,8 @@ bool contains(String haystack, String needle)
String replace(String s, String search, String replace_with)
{
if(search == "")
return(s);
s64 last_spos = 0;
auto spos = s.find(search);
if(spos == std::string::npos)
@@ -791,7 +793,7 @@ DValue array_merge(DValue a, DValue b)
StringList split_utf8(String s, bool compound_characters)
{
StringList result;
auto len = s.size();
s64 len = (s64)s.size();
String codepoint = "";
for(s64 i = 0; i < len; i++)
{
@@ -800,13 +802,13 @@ StringList split_utf8(String s, bool compound_characters)
{
codepoint = "";
codepoint.append(1, c);
if(is_bit_set(c, 6))
if(is_bit_set(c, 6) && i + 1 < len)
{
codepoint.append(1, s[++i]);
if(is_bit_set(c, 5))
if(is_bit_set(c, 5) && i + 1 < len)
{
codepoint.append(1, s[++i]);
if(is_bit_set(c, 4))
if(is_bit_set(c, 4) && i + 1 < len)
{
codepoint.append(1, s[++i]);
}
@@ -837,7 +839,7 @@ StringList split_utf8(String s, bool compound_characters)
join_next = true;
last_was_regional = false;
}
else if(s[0] == '\xF0' && s[1] == '\x9F' && s[2] == '\x87' && s[3] >= '\xA6' && s[3] <= '\xBF') // Regional indicator letters
else if(s.size() == 4 && s[0] == '\xF0' && s[1] == '\x9F' && s[2] == '\x87' && s[3] >= '\xA6' && s[3] <= '\xBF') // Regional indicator letters
{
if(last_was_regional)
{
@@ -850,7 +852,7 @@ StringList split_utf8(String s, bool compound_characters)
last_was_regional = true;
}
}
else if(s[0] == '\xEF' && s[1] == '\xB8' && s[2] >= '\x80' && s[2] <= '\x8F') // Variation selector
else if(s.size() == 3 && s[0] == '\xEF' && s[1] == '\xB8' && s[2] >= '\x80' && s[2] <= '\x8F') // Variation selector
{
compound_result[compound_result.size()-1] += s;
last_was_regional = false;
+148 -8
View File
@@ -423,12 +423,11 @@ bool crypto_equal_native(String a, String b)
return(diff == 0);
}
static bool uce_crypto_utf8_json_string(String value)
static bool uce_crypto_utf8_string(String value)
{
for(size_t i = 0; i < value.size();)
{
u8 c = (u8)value[i];
if(c < 0x20) return(false);
if(c < 0x80) { i++; continue; }
size_t need = c >= 0xC2 && c <= 0xDF ? 1 : (c >= 0xE0 && c <= 0xEF ? 2 : (c >= 0xF0 && c <= 0xF4 ? 3 : 0));
if(need == 0 || i + need >= value.size()) return(false);
@@ -440,6 +439,13 @@ static bool uce_crypto_utf8_json_string(String value)
return(true);
}
static bool uce_crypto_utf8_json_string(String value)
{
if(!uce_crypto_utf8_string(value)) return(false);
for(unsigned char c : value) if(c < 0x20) return(false);
return(true);
}
static bool uce_crypto_value_valid(const DValue& value, size_t depth, size_t& nodes, size_t& bytes)
{
if(depth > 16 || ++nodes > 256) return(false);
@@ -447,7 +453,7 @@ static bool uce_crypto_value_valid(const DValue& value, size_t depth, size_t& no
if(item.type == 'S')
{
bytes += item._String.size();
return(bytes <= 16384 && uce_crypto_utf8_json_string(item._String));
return(bytes <= 32768 && uce_crypto_utf8_json_string(item._String));
}
if(item.type == 'F') return(std::isfinite(item._float));
if(item.type == 'B') return(true);
@@ -458,7 +464,7 @@ static bool uce_crypto_value_valid(const DValue& value, size_t depth, size_t& no
if(!list)
{
bytes += child.first.size();
if(bytes > 16384 || !uce_crypto_utf8_json_string(child.first)) return(false);
if(bytes > 32768 || !uce_crypto_utf8_json_string(child.first)) return(false);
}
if(!uce_crypto_value_valid(child.second, depth + 1, nodes, bytes)) return(false);
}
@@ -588,6 +594,11 @@ static constexpr size_t UCE_ES256_SIGNATURE_BYTES = 64;
static constexpr size_t UCE_ES256_JSON_MAX = 16 * 1024;
static constexpr size_t UCE_ES256_VALUE_MAX = 256;
static constexpr size_t UCE_ES256_DEPTH_MAX = 16;
static constexpr size_t UCE_ES256_COORDINATE_BASE64URL_MAX = 43;
static constexpr size_t UCE_CBOR_MAX_BYTES = 16 * 1024;
static constexpr size_t UCE_CBOR_MAX_BASE64URL = 21846;
static constexpr size_t UCE_ES256_DER_MAX_BYTES = 144;
static constexpr size_t UCE_ES256_DER_BASE64URL_MAX = 192;
struct UcePkeyDeleter { void operator()(EVP_PKEY* value) const { EVP_PKEY_free(value); } };
struct UcePkeyCtxDeleter { void operator()(EVP_PKEY_CTX* value) const { EVP_PKEY_CTX_free(value); } };
@@ -604,9 +615,9 @@ static String uce_base64url_encode(const unsigned char* bytes, size_t size)
return(encoded);
}
static bool uce_base64url_decode(String encoded, String& decoded)
static bool uce_base64url_decode(String encoded, String& decoded, size_t max_encoded)
{
if(encoded.empty() || encoded.size() > 128 || encoded.find('=') != String::npos || encoded.size() % 4 == 1)
if(encoded.empty() || encoded.size() > max_encoded || encoded.find('=') != String::npos || encoded.size() % 4 == 1)
return(false);
for(char c : encoded)
if(!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '-' && c != '_')
@@ -651,7 +662,7 @@ static bool uce_es256_jwk_string(const DValue& jwk, const String& field, String&
if(!found)
return(false);
const DValue& item = found->deref();
if(item.type != 'S' || item._String.empty() || item._String.size() > 128)
if(item.type != 'S' || item._String.empty() || item._String.size() > 32768)
return(false);
value = item._String;
return(true);
@@ -663,7 +674,7 @@ static std::unique_ptr<EVP_PKEY, UcePkeyDeleter> uce_es256_key_from_jwk(const DV
return(nullptr);
String kty, crv, x64, y64, d64, x, y, d;
if(!uce_es256_jwk_string(jwk, "kty", kty) || !uce_es256_jwk_string(jwk, "crv", crv) || !uce_es256_jwk_string(jwk, "x", x64) || !uce_es256_jwk_string(jwk, "y", y64) || !uce_es256_jwk_string(jwk, "d", d64) ||
kty != "EC" || crv != "P-256" || !uce_base64url_decode(x64, x) || !uce_base64url_decode(y64, y) || !uce_base64url_decode(d64, d) ||
kty != "EC" || crv != "P-256" || !uce_base64url_decode(x64, x, UCE_ES256_COORDINATE_BASE64URL_MAX) || !uce_base64url_decode(y64, y, UCE_ES256_COORDINATE_BASE64URL_MAX) || !uce_base64url_decode(d64, d, UCE_ES256_COORDINATE_BASE64URL_MAX) ||
x.size() != UCE_ES256_COORDINATE_BYTES || y.size() != UCE_ES256_COORDINATE_BYTES || d.size() != UCE_ES256_COORDINATE_BYTES)
return(nullptr);
unsigned char public_key[65];
@@ -701,6 +712,87 @@ static bool uce_es256_key_coordinates(EVP_PKEY* key, String& x, String& y, Strin
return(true);
}
enum class UceCborKind { Unsigned, Negative, Bytes, Text, Array, Map };
struct UceCbor { UceCborKind kind; u64 number = 0; String bytes; std::vector<UceCbor> items; };
static constexpr size_t UCE_CBOR_MAX_NODES = 256, UCE_CBOR_MAX_DEPTH = 16;
static bool uce_cbor_uint(const String& in, size_t& p, u8 ai, u64& n)
{
if(ai < 24) { n = ai; return(true); }
size_t count = ai == 24 ? 1 : ai == 25 ? 2 : ai == 26 ? 4 : ai == 27 ? 8 : 0;
if(!count || p > in.size() || count > in.size() - p) return(false);
n = 0;
for(size_t i = 0; i < count; i++)
{
if(n > (UINT64_MAX - (u8)in[p + i]) / 256) return(false);
n = n * 256 + (u8)in[p + i];
}
p += count;
return((count == 1 && n >= 24) || (count == 2 && n > 0xff) || (count == 4 && n > 0xffff) || (count == 8 && n > 0xffffffff));
}
static bool uce_cbor_equal(const UceCbor& left, const UceCbor& right)
{
if(left.kind != right.kind || left.number != right.number || left.bytes != right.bytes || left.items.size() != right.items.size()) return(false);
for(size_t i = 0; i < left.items.size(); i++) if(!uce_cbor_equal(left.items[i], right.items[i])) return(false);
return(true);
}
static bool uce_cbor_read(const String& in, size_t& p, UceCbor& out, size_t depth, size_t& nodes)
{
if(p >= in.size() || depth > UCE_CBOR_MAX_DEPTH || ++nodes > UCE_CBOR_MAX_NODES) return(false);
u8 header = (u8)in[p++], major = header >> 5, ai = header & 31;
u64 n = 0;
if(ai == 31 || !uce_cbor_uint(in, p, ai, n)) return(false);
if(major == 0 || major == 1)
{
out.kind = major ? UceCborKind::Negative : UceCborKind::Unsigned;
out.number = n;
return(true);
}
if(major == 2 || major == 3)
{
if(n > UCE_CBOR_MAX_BYTES || n > in.size() - p || (major == 3 && !uce_crypto_utf8_string(String(in.data() + p, (size_t)n)))) return(false);
out.kind = major == 2 ? UceCborKind::Bytes : UceCborKind::Text;
out.bytes.assign(in.data() + p, (size_t)n);
p += (size_t)n;
return(true);
}
if((major != 4 && major != 5) || n > UCE_CBOR_MAX_NODES) return(false);
size_t item_count = (size_t)n * (major == 5 ? 2 : 1);
if(item_count > UCE_CBOR_MAX_NODES - nodes) return(false);
out.kind = major == 4 ? UceCborKind::Array : UceCborKind::Map;
out.items.reserve(item_count);
for(size_t i = 0; i < item_count; i++)
{
UceCbor child;
if(!uce_cbor_read(in, p, child, depth + 1, nodes)) return(false);
if(major == 5 && !(i & 1))
for(size_t previous = 0; previous < out.items.size(); previous += 2)
if(uce_cbor_equal(out.items[previous], child)) return(false);
out.items.push_back(std::move(child));
}
return(true);
}
static DValue uce_cbor_value(const UceCbor& value)
{
DValue out; switch(value.kind) { case UceCborKind::Unsigned: out["type"]="unsigned"; out["value"]=std::to_string(value.number); break; case UceCborKind::Negative: out["type"]="negative"; out["value"]=value.number==UINT64_MAX?"-18446744073709551616":"-"+std::to_string(value.number+1); break; case UceCborKind::Bytes: out["type"]="bytes"; out["base64url"]=uce_base64url_encode((const unsigned char*)value.bytes.data(),value.bytes.size()); break; case UceCborKind::Text: out["type"]="text"; out["value"]=value.bytes; break; case UceCborKind::Array: out["type"]="array"; out["items"].set_array(); for(const auto& x:value.items) out["items"].push(uce_cbor_value(x)); break; case UceCborKind::Map: out["type"]="map"; out["entries"].set_array(); for(size_t i=0;i<value.items.size();i+=2) { DValue entry; entry["key"]=uce_cbor_value(value.items[i]); entry["value"]=uce_cbor_value(value.items[i+1]); out["entries"].push(entry); } } return out;
}
static bool uce_cbor_es256(const UceCbor& cose,String& x,String& y)
{
if(cose.kind!=UceCborKind::Map) return false; const UceCbor *kty=0,*alg=0,*crv=0,*xx=0,*yy=0;
for(size_t i=0;i<cose.items.size();i+=2) { const UceCbor& k=cose.items[i]; const UceCbor& v=cose.items[i+1]; if(k.kind!=UceCborKind::Unsigned&&k.kind!=UceCborKind::Negative) continue; bool neg=k.kind==UceCborKind::Negative; if(!neg&&k.number==1) kty=&v; else if(!neg&&k.number==3) alg=&v; else if(neg&&k.number==0) crv=&v; else if(neg&&k.number==1) xx=&v; else if(neg&&k.number==2) yy=&v; }
return(kty&&alg&&crv&&xx&&yy&&kty->kind==UceCborKind::Unsigned&&kty->number==2&&alg->kind==UceCborKind::Negative&&alg->number==6&&crv->kind==UceCborKind::Unsigned&&crv->number==1&&xx->kind==UceCborKind::Bytes&&yy->kind==UceCborKind::Bytes&&xx->bytes.size()==32&&yy->bytes.size()==32&&(x=xx->bytes,true)&&(y=yy->bytes,true));
}
static std::unique_ptr<EVP_PKEY,UcePkeyDeleter> uce_es256_public_key(String x,String y)
{
if(x.size()!=32||y.size()!=32) return nullptr; unsigned char point[65]={4}; memcpy(point+1,x.data(),32); memcpy(point+33,y.data(),32); OSSL_PARAM p[]={OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME,(char*)"prime256v1",0),OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_PUB_KEY,point,sizeof(point)),OSSL_PARAM_construct_end()}; std::unique_ptr<EVP_PKEY_CTX,UcePkeyCtxDeleter> ctx(EVP_PKEY_CTX_new_from_name(0,"EC",0)); EVP_PKEY* raw=0; if(!ctx||EVP_PKEY_fromdata_init(ctx.get())<=0||EVP_PKEY_fromdata(ctx.get(),&raw,EVP_PKEY_PUBLIC_KEY,p)<=0) return nullptr; std::unique_ptr<EVP_PKEY,UcePkeyDeleter> key(raw); std::unique_ptr<EVP_PKEY_CTX,UcePkeyCtxDeleter> check(EVP_PKEY_CTX_new(key.get(),0)); return check&&EVP_PKEY_public_check(check.get())>0?std::move(key):nullptr;
}
static bool uce_es256_cose(String encoded,String& x,String& y)
{
String raw; UceCbor cose; size_t p=0,nodes=0; return uce_base64url_decode(encoded, raw, UCE_CBOR_MAX_BASE64URL) && raw.size() <= UCE_CBOR_MAX_BYTES && uce_cbor_read(raw, p, cose, 0, nodes) && p == raw.size() && uce_cbor_es256(cose, x, y) && uce_es256_public_key(x, y);
}
static DValue uce_es256_jwk(String x, String y, String d = "")
{
DValue jwk;
@@ -811,6 +903,54 @@ DValue crypto_operation_native(DValue request)
key["algorithm"] = algorithm;
return(key);
}
if(operation == "cbor_decode")
{
String encoded, raw; UceCbor value; size_t p=0,nodes=0;
if(!uce_es256_jwk_string(request, "cbor_base64url", encoded) || !uce_base64url_decode(encoded, raw, UCE_CBOR_MAX_BASE64URL) || raw.size() > UCE_CBOR_MAX_BYTES || !uce_cbor_read(raw, p, value, 0, nodes) || p != raw.size()) { result["error"] = "invalid_cbor"; return(result); }
result["ok"].set_bool(true); result["operation"]=operation; result["value"]=uce_cbor_value(value); return result;
}
if(operation == "cose_es256_parse")
{
String encoded,x,y; if(!uce_es256_jwk_string(request,"cose_key_base64url",encoded)||!uce_es256_cose(encoded,x,y)) { result["error"]="invalid_cose_key"; return result; }
result["ok"].set_bool(true); result["operation"]=operation; result["algorithm"]=algorithm; result["x_base64url"]=uce_base64url_encode((const unsigned char*)x.data(),x.size()); result["y_base64url"]=uce_base64url_encode((const unsigned char*)y.data(),y.size()); return result;
}
if(operation == "es256_verify")
{
String encoded, message64, signature64, x, y, message, der;
if(!uce_es256_jwk_string(request, "cose_key_base64url", encoded) || !uce_es256_jwk_string(request, "message_base64url", message64) || !uce_es256_jwk_string(request, "signature_der_base64url", signature64) || !uce_es256_cose(encoded, x, y) || !uce_base64url_decode(message64, message, UCE_CBOR_MAX_BASE64URL) || !uce_base64url_decode(signature64, der, UCE_ES256_DER_BASE64URL_MAX) || message.size() > UCE_CBOR_MAX_BYTES || der.empty() || der.size() > UCE_ES256_DER_MAX_BYTES)
{
result["error"] = "invalid_key_or_payload";
return(result);
}
const unsigned char* cursor = (const unsigned char*)der.data();
std::unique_ptr<ECDSA_SIG, UceEcdsaSigDeleter> signature(d2i_ECDSA_SIG(0, &cursor, der.size()));
int canonical = signature ? i2d_ECDSA_SIG(signature.get(), 0) : 0;
String reencoded(canonical > 0 ? (size_t)canonical : 0, 0);
unsigned char* dest = (unsigned char*)reencoded.data();
if(!signature || cursor != (const unsigned char*)der.data() + der.size() || canonical <= 0 || i2d_ECDSA_SIG(signature.get(), &dest) != canonical || reencoded != der)
{
result["error"] = "invalid_signature";
return(result);
}
std::unique_ptr<EVP_PKEY, UcePkeyDeleter> key = uce_es256_public_key(x, y);
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free);
if(!key || !ctx || EVP_DigestVerifyInit(ctx.get(), 0, EVP_sha256(), 0, key.get()) <= 0)
{
result["error"] = "operation_failed";
return(result);
}
int verified = EVP_DigestVerify(ctx.get(), (const unsigned char*)der.data(), der.size(), (const unsigned char*)message.data(), message.size());
if(verified < 0)
{
result["error"] = "operation_failed";
return(result);
}
result["ok"].set_bool(true);
result["operation"] = operation;
result["algorithm"] = algorithm;
result["valid"].set_bool(verified == 1);
return(result);
}
if(operation == "jwt_sign")
{
String jwt = uce_es256_jwt(request["private_jwk"], request["protected_header"], request["claims"]);
+48 -10
View File
@@ -246,9 +246,10 @@ DValue MySQL::query(String q)
statement_info = "mysql connection is not open";
return(DValue());
}
_preload_next_error_code = mysql_query((MYSQL*)connection, q.c_str());
int query_status = mysql_query((MYSQL*)connection, q.c_str());
_preload_next_error_code = query_status == 0 ? 0 : mysql_errno((MYSQL*)connection);
DValue result;
if(_preload_next_error_code == 0)
if(query_status == 0)
result = get_pending_result();
return(result);
}
@@ -291,11 +292,16 @@ static bool mysql_has_unquoted_positional_placeholder(String query)
DValue MySQL::query(String q, StringMap params)
{
// Positional ? placeholders survive named substitution (values are always
// quoted by escape()), so the check in query(String) covers this path too.
return(query(
parse_query_parameters(q, params).c_str()
));
// Positional ? placeholders survive named substitution (ordinary values
// are quoted by escape()), so the check in query(String) covers this path.
parameter_error = false;
String parsed = parse_query_parameters(q, params);
if(parameter_error)
{
_preload_next_error_code = CR_UNKNOWN_ERROR;
return(DValue());
}
return(query(parsed));
}
String MySQL::parse_query_parameters(String query, StringMap map)
@@ -305,6 +311,7 @@ String MySQL::parse_query_parameters(String query, StringMap map)
u8 mode = 0;
char quote;
bool escaped = false;
String identifier;
for(u32 i = 0; i < query.length(); i++)
{
@@ -321,6 +328,7 @@ String MySQL::parse_query_parameters(String query, StringMap map)
result.append(1, c);
mode = 2;
quote = c;
escaped = false;
}
else
{
@@ -329,10 +337,25 @@ String MySQL::parse_query_parameters(String query, StringMap map)
}
else if(mode == 1) // identifier mode
{
if(isalnum(c) || c == '_')
if(isalnum((unsigned char)c) || c == '_')
{
identifier.append(1, c);
}
else if(c == '!' && query[i + 1] != '=')
{
String value = map[identifier];
bool valid = identifier != "" && value != "";
for(char digit : value)
if(!isdigit((unsigned char)digit)) valid = false;
if(!valid)
{
parameter_error = true;
statement_info = "mysql unsigned parameter :" + identifier + "! must contain only decimal digits";
return("");
}
result.append(value);
mode = 0;
}
else
{
result.append(escape(map[identifier]));
@@ -342,9 +365,19 @@ String MySQL::parse_query_parameters(String query, StringMap map)
}
else if(mode == 2) // quoted mode
{
result.append(1, c);
if(escaped)
{
escaped = false;
continue;
}
if(c == '\\')
{
escaped = true;
continue;
}
if(c == quote)
mode = 0;
result.append(1, c);
}
}
@@ -390,10 +423,15 @@ String MySQL::error()
case(CR_OUT_OF_MEMORY):
p = "Out of memory";
break;
default:
case(CR_UNKNOWN_ERROR):
p = "Unknown server error";
break;
default:
if(connection && mysql_error((MYSQL*)connection)[0] != '\0')
p = mysql_error((MYSQL*)connection);
else
p = "Unknown server error";
break;
}
_preload_next_error_code = 0;
return(p);
+1
View File
@@ -20,6 +20,7 @@ struct MySQL {
u32 row_count = 0;
u64 insert_id = 0;
String statement_info = ""; //
bool parameter_error = false;
bool request_cleanup_delete = false;
bool request_pooled = false;
bool worker_persistent = false;
+3
View File
@@ -325,6 +325,7 @@ u64 http_request_async(DValue req)
return((u64)uce_host_http_request_async(encoded.data(), encoded.size()));
}
DValue shell_exec(DValue spec)
{
String encoded = ucb_encode(spec);
@@ -778,6 +779,7 @@ DValue crypto_operation(DValue request) { return(crypto_operation_native(request
// Single definitions for the native split build (declared extern in sys.h).
pid_t parent_pid = 0;
pid_t my_pid = 0;
bool task_child_process = false;
namespace {
@@ -1996,6 +1998,7 @@ pid_t task(String key, std::function<void()> exec_after_spawn, u64 timeout)
{
close_locked_file(lock_fd);
my_pid = getpid();
task_child_process = true;
// The FastCGI worker handles termination to drain accepted requests.
// Generic task children do not run that drain loop, so inheriting those
// handlers would turn task_kill(SIGTERM) into a no-op.
+2
View File
@@ -149,9 +149,11 @@ StringMap memcache_get_multiple(u64 connection, StringList keys);
#if defined(__UCE_WASM_CORE__) || defined(__UCE_WASM_UNIT__)
pid_t parent_pid = 0;
pid_t my_pid = 0;
bool task_child_process = false;
#else
extern pid_t parent_pid;
extern pid_t my_pid;
extern bool task_child_process;
#endif
void on_segfault(int sig);
+5 -1
View File
@@ -97,7 +97,11 @@ String base64_decode(String raw, bool& ok)
return("");
if(padding > 0 && i + 4 != cleaned.length())
return("");
if(cleaned[i + 2] == '=' && cleaned[i + 3] != '=')
if(cleaned[i] == '=' || cleaned[i + 1] == '=')
return("");
if(padding == 1 && (cleaned[i + 2] == '=' || cleaned[i + 3] != '='))
return("");
if(padding == 2 && (cleaned[i + 2] != '=' || cleaned[i + 3] != '='))
return("");
result.append(1, (char)((values[0] << 2) | (values[1] >> 4)));
+2 -1
View File
@@ -1132,7 +1132,8 @@ void run_ws_broker()
if(server_state.config["WS_BROKER_SOCKET_PATH"] != "")
{
ws_broker.listen(server_state.config["WS_BROKER_SOCKET_PATH"]);
chmod(server_state.config["WS_BROKER_SOCKET_PATH"].c_str(), S_IRWXU | S_IRGRP | S_IWGRP);
if(chmod(server_state.config["WS_BROKER_SOCKET_PATH"].c_str(), S_IRWXU | S_IRGRP | S_IWGRP) != 0)
fprintf(stderr, "(!) Could not chmod socket %s to %04o: %s\n", server_state.config["WS_BROKER_SOCKET_PATH"].c_str(), 0760, strerror(errno));
}
while(!termination_signal_received)
{
+40 -3
View File
@@ -207,6 +207,7 @@ String MySQL::parse_query_parameters(String query, StringMap map)
u8 mode = 0;
char quote = 0;
bool escaped = false;
String identifier;
for(u32 i = 0; i < query.length(); i++)
{
@@ -223,14 +224,30 @@ String MySQL::parse_query_parameters(String query, StringMap map)
result.append(1, c);
mode = 2;
quote = c;
escaped = false;
}
else
result.append(1, c);
}
else if(mode == 1)
{
if(isalnum(c) || c == '_')
if(isalnum((unsigned char)c) || c == '_')
identifier.append(1, c);
else if(c == '!' && query[i + 1] != '=')
{
String value = map[identifier];
bool valid = identifier != "" && value != "";
for(char digit : value)
if(!isdigit((unsigned char)digit)) valid = false;
if(!valid)
{
parameter_error = true;
statement_info = "mysql unsigned parameter :" + identifier + "! must contain only decimal digits";
return("");
}
result.append(value);
mode = 0;
}
else
{
result.append(escape(map[identifier]));
@@ -240,9 +257,19 @@ String MySQL::parse_query_parameters(String query, StringMap map)
}
else if(mode == 2)
{
result.append(1, c);
if(escaped)
{
escaped = false;
continue;
}
if(c == '\\')
{
escaped = true;
continue;
}
if(c == quote)
mode = 0;
result.append(1, c);
}
}
@@ -315,7 +342,17 @@ DValue MySQL::query(String q)
return(result ? *result : DValue());
}
DValue MySQL::query(String q, StringMap params) { return(query(parse_query_parameters(q, params))); }
DValue MySQL::query(String q, StringMap params)
{
parameter_error = false;
String parsed = parse_query_parameters(q, params);
if(parameter_error)
{
_preload_next_error_code = 2000;
return(DValue());
}
return(query(parsed));
}
DValue MySQL::get_pending_result() { return(DValue()); }
// sqlite runs host-side (the host links libsqlite and owns the connections in
+146
View File
@@ -0,0 +1,146 @@
// Internal worker-local implementation for opt-in generic hardened HTTP.
#pragma once
#include <arpa/inet.h>
#include <cctype>
#include <cerrno>
#include <fcntl.h>
#include <functional>
#include <signal.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <unistd.h>
static u64 hardened_http_monotonic_ms() { timespec ts{}; clock_gettime(CLOCK_MONOTONIC,&ts); return (u64)ts.tv_sec*1000ull+(u64)ts.tv_nsec/1000000ull; }
static void hardened_http_close_inherited_fds()
{
#ifdef SYS_close_range
if(syscall(SYS_close_range,4u,~0u,0u)==0) return;
#endif
long max_fd=sysconf(_SC_OPEN_MAX); if(max_fd<4) max_fd=4;
for(int fd=4;fd<max_fd;fd++) close(fd);
}
struct HardenedHttpExecResult { int exit_code=-1; bool timed_out=false, output_limited=false; String headers_text, body_text, stderr_text; };
struct HardenedHttpHooks {
std::function<std::vector<String>(String)> resolve;
std::function<HardenedHttpExecResult(std::vector<String>, String, std::vector<String>, u64, size_t)> execute;
};
// Hardened requests deliberately support public IPv4 answers only. Every IPv6
// answer fails closed until this generic transport has a reviewed IPv6 policy.
// IPv4 must be globally unicast: reject IANA special-purpose 0/8, 10/8,
// 100.64/10, 127/8, 169.254/16, 172.16/12, 192.0.0/24, 192.0.2/24,
// 192.31.196/24, 192.52.193/24, 192.88.99/24, 192.175.48/24, 192.168/16,
// 198.18/15, 198.51.100/24, 203.0.113/24, and all 224/4 multicast/reserved.
static bool hardened_http_public_address(String text)
{
in_addr v4{};
if(inet_pton(AF_INET,text.c_str(),&v4)!=1) return false; // Includes every AAAA candidate.
u32 x=ntohl(v4.s_addr); u8 a=x>>24,b=x>>16,c=x>>8;
if(a==0||a==10||a==127||a>=224||(a==100&&b>=64&&b<=127)||(a==169&&b==254)||(a==172&&b>=16&&b<=31)||(a==192&&(b==0||b==2||b==168))||(a==192&&b==31&&c==196)||(a==192&&b==52&&c==193)||(a==192&&b==88&&c==99)||(a==192&&b==175&&c==48)||(a==198&&(b==18||b==19||b==51))||(a==203&&b==0&&c==113)) return false;
return true;
}
// Three private pipes keep curl's -D headers and -o body distinct. The child
// inherits only stdin/stdout/stderr and the header pipe on fd 3. Async workers
// retain their own process group so job_cancel can kill curl and its descendants.
static HardenedHttpExecResult hardened_http_exec_argv_capture(std::vector<String> argv, String input, u64 timeout_ms, size_t output_limit, bool clean_env=true, bool own_process_group=true)
{
HardenedHttpExecResult r; if(argv.empty()) return r;
int in[2], body[2], headers[2], err[2];
if(pipe(in)||pipe(body)||pipe(headers)||pipe(err)) return r;
pid_t pid=fork();
if(pid==0) {
if(own_process_group) setpgid(0,0);
dup2(in[0],0); dup2(body[1],1); dup2(err[1],2); dup2(headers[1],3);
hardened_http_close_inherited_fds();
std::vector<char*> args; for(String& a:argv) args.push_back((char*)a.c_str()); args.push_back(0);
if(clean_env) { clearenv(); setenv("PATH","/usr/bin:/bin",1); execv(args[0],args.data()); } else execvp(args[0],args.data());
_exit(127);
}
auto close_all=[&](){ for(int fd: {in[0],in[1],body[0],body[1],headers[0],headers[1],err[0],err[1]}) close(fd); };
if(pid<0) { close_all(); return r; }
if(own_process_group) setpgid(pid,pid);
close(in[0]); close(body[1]); close(headers[1]); close(err[1]);
for(int fd: {in[1],body[0],headers[0],err[0]}) fcntl(fd,F_SETFL,fcntl(fd,F_GETFL,0)|O_NONBLOCK);
size_t input_off=0, total=0; bool in_open=true,body_open=true,headers_open=true,err_open=true,exited=false,killed=false; int status=0; u64 deadline=hardened_http_monotonic_ms()+timeout_ms;
auto terminate=[&](bool timeout){ if(timeout) r.timed_out=true; if(!killed) { if(own_process_group) kill(-pid,SIGKILL); kill(pid,SIGKILL); killed=true; } };
auto drain=[&](int fd,bool& open,String& dst) { char buf[4096]; ssize_t n; while((n=read(fd,buf,sizeof(buf)))>0) { if(total+(size_t)n>output_limit) { r.output_limited=true; terminate(false); } else { dst+=String(buf,n); total+=(size_t)n; } } if(n==0) { close(fd); open=false; } };
while(in_open||body_open||headers_open||err_open||!exited) {
if(!exited) { pid_t w=waitpid(pid,&status,WNOHANG); if(w==pid) exited=true; }
if(in_open) { if(input_off<input.size()) { ssize_t n=write(in[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(in[1]); in_open=false; } } else { close(in[1]); in_open=false; } }
drain(body[0],body_open,r.body_text); drain(headers[0],headers_open,r.headers_text); drain(err[0],err_open,r.stderr_text);
if(!killed&&hardened_http_monotonic_ms()>=deadline) terminate(true);
if(killed&&!exited) { while(waitpid(pid,&status,0)<0&&errno==EINTR) {} exited=true; }
if(in_open||body_open||headers_open||err_open||!exited) usleep(1000);
}
if(WIFEXITED(status)) r.exit_code=WEXITSTATUS(status); else if(WIFSIGNALED(status)) r.exit_code=128+WTERMSIG(status);
return r;
}
static bool hardened_http_parse_headers(String text, u64& status, DValue& filtered)
{
status=0; filtered.set_array(); size_t pos=0; bool found=false;
while(pos<text.size()) {
size_t end=text.find("\r\n\r\n",pos); size_t sep=4; if(end==String::npos) { end=text.find("\n\n",pos); sep=2; } if(end==String::npos) return false;
String block=replace(text.substr(pos,end-pos),"\r",""); size_t nl=block.find('\n'); String first=nl==String::npos?block:block.substr(0,nl);
if(first.size()<12||first.rfind("HTTP/",0)!=0||first[8]!=' '||!isdigit((unsigned char)first[9])||!isdigit((unsigned char)first[10])||!isdigit((unsigned char)first[11])) return false;
status=strtoull(first.substr(9,3).c_str(),0,10); DValue current; current.set_array();
if(nl!=String::npos) for(String line:split(block.substr(nl+1),"\n")) { size_t c=line.find(':'); if(c==String::npos) return false; String k=to_lower(trim(line.substr(0,c))); if(k=="content-type"||k=="cache-control") current[k]=trim(line.substr(c+1)); }
filtered=current; found=true; pos=end+sep;
}
return found;
}
static bool hardened_http_token(String text)
{
if(text=="") return false;
for(unsigned char c:text) if(!(isalnum(c)||c=='-'||c=='_')) return false;
return true;
}
static bool hardened_http_header_value(String text)
{
if(text.size()>8192||text.find('\0')!=String::npos||text.find('\r')!=String::npos||text.find('\n')!=String::npos) return false;
return true;
}
static bool hardened_http_ip_literal(String text)
{
in_addr v4{}; in6_addr v6{};
return inet_pton(AF_INET,text.c_str(),&v4)==1 || inet_pton(AF_INET6,text.c_str(),&v6)==1;
}
static bool hardened_http_url(String url, String& host, String& port)
{
if(url.find('\0')!=String::npos||url.find_first_of("\\ \r\n\t")!=String::npos||url.rfind("https://",0)!=0) return false;
String rest=url.substr(8); size_t slash=rest.find('/'); String authority=slash==String::npos?rest:rest.substr(0,slash);
if(authority==""||authority.find('@')!=String::npos||authority.find('[')!=String::npos||authority.find(']')!=String::npos) return false;
size_t colon=authority.rfind(':'); host=colon==String::npos?authority:authority.substr(0,colon); port=colon==String::npos?"443":authority.substr(colon+1);
if(host==""||hardened_http_ip_literal(host)||!hardened_http_token(port)) return false;
for(unsigned char c:host) if(!(isalnum(c)||c=='.'||c=='-')) return false;
char* end=0; unsigned long n=strtoul(port.c_str(),&end,10); return end!=port.c_str()&&*end==0&&n>0&&n<=65535;
}
static bool hardened_http_security_requested(const DValue* security)
{
return security && (security->key("https_only") || security->key("public_dns_only") || security->key("pin_dns") || security->key("isolated_curl") || security->key("no_redirects"));
}
static bool hardened_http_true(const DValue* value)
{
return value && value->get_type_name()=="bool" && value->to_bool();
}
static DValue hardened_http_request_internal(const DValue& req, u64 timeout_ms, const HardenedHttpHooks& hooks)
{
DValue r; r["status"]=(f64)0; r["headers"].set_array(); r["body"]=""; r["error"]="";
const DValue* sec=req.key("security");
if(req.key("follow_redirects") && req.key("follow_redirects")->to_bool()) { r["error"]="invalid_request"; return r; }
timeout_ms=std::min<u64>(std::max<u64>(1,timeout_ms),10000);
if(!sec || !hardened_http_true(sec->key("https_only")) || !hardened_http_true(sec->key("public_dns_only")) || !hardened_http_true(sec->key("pin_dns")) || !hardened_http_true(sec->key("isolated_curl")) || !hardened_http_true(sec->key("no_redirects"))) { r["error"]="invalid_request"; return r; }
String method=req.key("method")?req.key("method")->to_string():"GET"; String url=req.key("url")?req.key("url")->to_string():""; String host,port;
if((method!="GET"&&method!="POST"&&method!="PUT"&&method!="PATCH"&&method!="DELETE"&&method!="HEAD"&&method!="OPTIONS")||!hardened_http_url(url,host,port)) { r["error"]="invalid_request"; return r; }
String body=req.key("body")?req.key("body")->to_string():""; if(body.size()>65536||body.find('\0')!=String::npos) { r["error"]="invalid_request"; return r; }
std::vector<String> argv={"/usr/bin/curl","--disable","-sS","--http1.1","--proto","=https","--proto-redir","=https","--noproxy","*","--proxy","","--alt-svc","","--hsts","","--cacert","/etc/ssl/certs/ca-certificates.crt","--connect-timeout","3","--max-time",std::to_string(std::max<u64>(1,timeout_ms/1000)),"--max-filesize","65536","-X",method,"-D","/proc/self/fd/3","-o","/proc/self/fd/1"};
const DValue* hs=req.key("headers"); if(hs) { bool valid=true; hs->each([&](const DValue& v,String k) { String value=v.to_string(), lower=to_lower(k); if(!hardened_http_token(k)||!hardened_http_header_value(value)||lower=="host"||lower=="content-length"||lower=="transfer-encoding"||lower=="connection"||lower=="proxy-connection"||lower=="expect") valid=false; else { argv.push_back("-H"); argv.push_back(k+": "+value); } }); if(!valid) { r["error"]="invalid_request"; return r; } }
std::vector<String> answers=hooks.resolve(host); String address; for(String a:answers) { if(!hardened_http_public_address(a)) { r["error"]="unsafe_dns"; return r; } if(address=="") address=a; } if(address=="") { r["error"]="unsafe_dns"; return r; }
argv.push_back("--resolve"); argv.push_back(host+":"+port+":"+address); if(req.key("body")) { argv.push_back("--data-binary"); argv.push_back("@-"); } argv.push_back(url);
HardenedHttpExecResult pr=hooks.execute(argv,body,{"PATH=/usr/bin:/bin"},timeout_ms,72*1024); if(pr.output_limited||pr.body_text.size()>65536||pr.headers_text.size()>8192) { r["error"]="response_too_large"; return r; }
u64 status; DValue headers; if(!hardened_http_parse_headers(pr.headers_text,status,headers)) { r["error"]=pr.timed_out?"timeout":"malformed_output"; return r; }
r["status"]=(f64)status; r["headers"]=headers; r["body"]=pr.body_text;
if(status/100==3) r["error"]="redirect_not_allowed"; else if(pr.exit_code!=0) r["error"]=pr.timed_out?"timeout":"network_failure"; else if(status/100!=2) r["error"]="http_status"; return r;
}
+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 != "")