Phase 5/6/7B: DNS sockets, crypto random, and display lowering
ober
348a782cdbe2d296594f60c9e534e6ece197b31a
--- a/jerboa-native-rs/Cargo.lock +++ b/jerboa-native-rs/Cargo.lock @@ -481,6 +481,7 @@ dependencies = [ "argon2", "ed25519-dalek", "flate2", + "getrandom 0.2.17", "hkdf", "inotify", "libc", --- a/jerboa-native-rs/Cargo.toml +++ b/jerboa-native-rs/Cargo.toml @@ -26,6 +26,7 @@ rustls-pki-types = "1" rustls-pemfile = "2" webpki-roots = "0.26" wasmi = "0.40" +getrandom = "0.2" [target.'cfg(target_os = "linux")'.dependencies] inotify = { version = "0.11", default-features = false } --- a/jerboa-native-rs/src/wasm.rs +++ b/jerboa-native-rs/src/wasm.rs @@ -34,6 +34,14 @@ struct HostState { start_time: std::time::Instant, /// Log buffer for captured log_message calls log_buffer: Vec<String>, + /// UDP socket for DNS recv_packet / send_packet + udp_socket: Option<std::net::UdpSocket>, + /// Last peer address seen in recv_from (used by send_packet) + peer_addr: Option<std::net::SocketAddr>, + /// Open CDB file handles: handle → raw CDB data + cdb_handles: HashMap<i32, Vec<u8>>, + /// Counter for allocating CDB handles + next_cdb_handle: i32, } impl Default for HostState { @@ -41,6 +49,10 @@ impl Default for HostState { HostState { start_time: std::time::Instant::now(), log_buffer: Vec::new(), + udp_socket: None, + peer_addr: None, + cdb_handles: HashMap::new(), + next_cdb_handle: 0, } } } @@ -196,6 +208,42 @@ pub extern "C" fn jerboa_wasm_instance_free(handle: u64) { let _ = wasm_instances().lock().unwrap().remove(&handle); } +/// Attach a pre-opened UDP socket fd to a hosted WASM instance. +/// +/// After this call, recv_packet / send_packet host imports use this socket. +/// The Scheme side opens the socket via standard OS calls and passes the fd. +/// +/// SAFETY: `fd` must be a valid, owned UDP socket file descriptor. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_set_socket(instance_handle: u64, fd: i32) -> i32 { + ffi_wrap(|| { + let mut instances = wasm_instances().lock().unwrap(); + let inst = match instances.get_mut(&instance_handle) { + Some(i) => i, + None => { + set_last_error("invalid instance handle".to_string()); + return -1; + } + }; + #[cfg(unix)] + { + use std::os::unix::io::FromRawFd; + // SAFETY: caller must ensure fd is a valid, owned UDP socket fd. + let socket = unsafe { std::net::UdpSocket::from_raw_fd(fd) }; + inst.store.data_mut().udp_socket = Some(socket); + inst.store.data_mut().peer_addr = None; + 0 + } + #[cfg(not(unix))] + { + let _ = fd; + set_last_error("jerboa_wasm_set_socket: not supported on this platform".to_string()); + -1 + } + }) +} + /// Add fuel to an existing instance. /// Returns 0 on success, -1 on error. #[no_mangle] @@ -495,6 +543,75 @@ pub extern "C" fn jerboa_wasm_memory_size(handle: u64) -> i64 { // Hosted instance: instantiate with WASI + DNS host imports // ============================================================ +// ============================================================ +// CDB (Constant Database) support +// ============================================================ + +/// CDB hash function (djb2 variant used by the CDB format). +fn cdb_hash(key: &[u8]) -> u32 { + let mut h: u32 = 5381; + for &b in key { + h = h.wrapping_shl(5).wrapping_add(h) ^ (b as u32); + } + h +} + +/// Look up a key in CDB-format data. Returns the value bytes on hit. +/// +/// CDB format: +/// Header (2048 bytes): 256 × (tbl_pos: u32 LE, tbl_len: u32 LE) +/// Records: key_len(4) val_len(4) key_bytes val_bytes +/// Hash tables at tbl_pos: tbl_len × (hash: u32 LE, rec_pos: u32 LE) +/// rec_pos == 0 means empty slot +fn cdb_lookup(data: &[u8], key: &[u8]) -> Option<Vec<u8>> { + if data.len() < 2048 { + return None; + } + let hash = cdb_hash(key); + let bucket = (hash & 0xFF) as usize; + let header_pos = bucket * 8; + + let tbl_pos = u32::from_le_bytes(data[header_pos..header_pos+4].try_into().ok()?) as usize; + let tbl_len = u32::from_le_bytes(data[header_pos+4..header_pos+8].try_into().ok()?) as usize; + + if tbl_len == 0 || tbl_pos == 0 { + return None; + } + + let start_slot = ((hash >> 8) as usize) % tbl_len; + + for i in 0..tbl_len { + let slot = (start_slot + i) % tbl_len; + let entry_pos = tbl_pos + slot * 8; + if entry_pos + 8 > data.len() { + return None; + } + let entry_hash = u32::from_le_bytes(data[entry_pos..entry_pos+4].try_into().ok()?); + let rec_pos = u32::from_le_bytes(data[entry_pos+4..entry_pos+8].try_into().ok()?) as usize; + + if rec_pos == 0 { + return None; // empty slot — key not found + } + + if entry_hash == hash { + if rec_pos + 8 > data.len() { + return None; + } + let klen = u32::from_le_bytes(data[rec_pos..rec_pos+4].try_into().ok()?) as usize; + let vlen = u32::from_le_bytes(data[rec_pos+4..rec_pos+8].try_into().ok()?) as usize; + let k_start = rec_pos + 8; + let v_start = k_start + klen; + if v_start + vlen > data.len() { + return None; + } + if &data[k_start..k_start + klen] == key { + return Some(data[v_start..v_start + vlen].to_vec()); + } + } + } + None +} + /// Define WASI-compatible and DNS host imports on a linker. fn define_host_imports(linker: &mut Linker<HostState>) -> Result<(), Error> { // ---- WASI: fd_write (fd, iovs_ptr, iovs_len, nwritten_ptr) -> errno ---- @@ -606,22 +723,14 @@ fn define_host_imports(linker: &mut Linker<HostState>) -> Result<(), Error> { Some(Extern::Memory(m)) => m, _ => return 8, }; - let seed = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() as u64; - let mut state = seed; let mem_data = memory.data_mut(&mut caller); let start = buf_ptr as usize; - let end = start + buf_len as usize; + let end = start + buf_len.max(0) as usize; if end > mem_data.len() { return 21; } - for byte in mem_data[start..end].iter_mut() { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - *byte = state as u8; + match getrandom::getrandom(&mut mem_data[start..end]) { + Ok(()) => 0, + Err(_) => 29, // ENOSYS — fall back to caller handling } - 0 } )?; @@ -668,36 +777,165 @@ fn define_host_imports(linker: &mut Linker<HostState>) -> Result<(), Error> { )?; // ---- DNS: recv_packet (buf_ptr, buf_max) -> packet_len ---- - // Stub: returns -1. Real usage requires socket integration. + // Blocks until a UDP packet arrives on the pre-opened socket. + // Returns the packet length on success, -1 on error. linker.func_wrap( "dns", "recv_packet", - |_caller: Caller<'_, HostState>, _buf_ptr: i32, _buf_max: i32| -> i32 { -1 } + |mut caller: Caller<'_, HostState>, buf_ptr: i32, buf_max: i32| -> i32 { + let memory = match caller.get_export("memory") { + Some(Extern::Memory(m)) => m, + _ => return -1, + }; + let max = buf_max.max(0) as usize; + let mut tmp = vec![0u8; max]; + // Clone the socket to release the borrow on caller before writing memory. + let cloned = caller.data().udp_socket.as_ref() + .and_then(|s| s.try_clone().ok()); + let sock = match cloned { + Some(s) => s, + None => return -1, + }; + match sock.recv_from(&mut tmp) { + Ok((n, addr)) => { + caller.data_mut().peer_addr = Some(addr); + let mem = memory.data_mut(&mut caller); + let start = buf_ptr as usize; + if start + n > mem.len() { return -1; } + mem[start..start + n].copy_from_slice(&tmp[..n]); + n as i32 + } + Err(_) => -1, + } + } )?; // ---- DNS: send_packet (buf_ptr, buf_len, addr_ptr, addr_len) -> bytes_sent ---- + // addr_ptr/addr_len: optional "ip:port" string in WASM memory. + // If addr_len == 0, uses the peer address saved by the last recv_packet. linker.func_wrap( "dns", "send_packet", - |_caller: Caller<'_, HostState>, - _buf_ptr: i32, _buf_len: i32, _addr_ptr: i32, _addr_len: i32| -> i32 { -1 } + |caller: Caller<'_, HostState>, + buf_ptr: i32, buf_len: i32, addr_ptr: i32, addr_len: i32| -> i32 { + let memory = match caller.get_export("memory") { + Some(Extern::Memory(m)) => m, + _ => return -1, + }; + // Copy packet bytes out of WASM memory. + let packet = { + let data = memory.data(&caller); + let start = buf_ptr as usize; + let end = start + buf_len.max(0) as usize; + if end > data.len() { return -1; } + data[start..end].to_vec() + }; + // Optionally parse destination address from WASM memory. + let dest_addr: Option<std::net::SocketAddr> = if addr_len > 0 { + let addr_str = { + let data = memory.data(&caller); + let start = addr_ptr as usize; + let end = start + addr_len as usize; + if end > data.len() { return -1; } + std::str::from_utf8(&data[start..end]).ok().map(str::to_string) + }; + addr_str.and_then(|s| s.parse().ok()) + } else { + None + }; + // Prefer explicit address; fall back to saved peer from last recv. + let target = dest_addr.or_else(|| caller.data().peer_addr); + let cloned = caller.data().udp_socket.as_ref() + .and_then(|s| s.try_clone().ok()); + match (cloned, target) { + (Some(sock), Some(addr)) => { + match sock.send_to(&packet, addr) { + Ok(n) => n as i32, + Err(_) => -1, + } + } + _ => -1, + } + } )?; // ---- DNS: cdb_open (path_ptr, path_len) -> handle ---- + // Reads the entire CDB file into memory and returns a handle (>=0). + // Returns -1 on error (file not found, I/O error, etc.). linker.func_wrap( "dns", "cdb_open", - |_caller: Caller<'_, HostState>, _path_ptr: i32, _path_len: i32| -> i32 { -1 } + |mut caller: Caller<'_, HostState>, path_ptr: i32, path_len: i32| -> i32 { + let memory = match caller.get_export("memory") { + Some(Extern::Memory(m)) => m, + _ => return -1, + }; + let path = { + let data = memory.data(&caller); + let start = path_ptr as usize; + let end = start + path_len.max(0) as usize; + if end > data.len() { return -1; } + match std::str::from_utf8(&data[start..end]) { + Ok(s) => s.to_string(), + Err(_) => return -1, + } + }; + let cdb_data = match std::fs::read(&path) { + Ok(d) => d, + Err(_) => return -1, + }; + let state = caller.data_mut(); + let handle = state.next_cdb_handle; + state.next_cdb_handle += 1; + state.cdb_handles.insert(handle, cdb_data); + handle + } )?; // ---- DNS: cdb_find (handle, key_ptr, key_len, val_buf, val_max) -> val_len ---- + // Looks up key in the CDB; copies value bytes to val_buf (up to val_max). + // Returns value length on hit, 0 on miss, -1 on error. linker.func_wrap( "dns", "cdb_find", - |_caller: Caller<'_, HostState>, - _handle: i32, _key_ptr: i32, _key_len: i32, _val_buf: i32, _val_max: i32| -> i32 { -1 } + |mut caller: Caller<'_, HostState>, + handle: i32, key_ptr: i32, key_len: i32, val_buf: i32, val_max: i32| -> i32 { + let memory = match caller.get_export("memory") { + Some(Extern::Memory(m)) => m, + _ => return -1, + }; + let key = { + let data = memory.data(&caller); + let start = key_ptr as usize; + let end = start + key_len.max(0) as usize; + if end > data.len() { return -1; } + data[start..end].to_vec() + }; + let val = { + let state = caller.data(); + match state.cdb_handles.get(&handle) { + Some(cdb_data) => cdb_lookup(cdb_data, &key), + None => return -1, + } + }; + match val { + None => 0, // key not found + Some(v) => { + let n = v.len().min(val_max.max(0) as usize); + let mem = memory.data_mut(&mut caller); + let start = val_buf as usize; + if start + n > mem.len() { return -1; } + mem[start..start + n].copy_from_slice(&v[..n]); + n as i32 + } + } + } )?; // ---- DNS: cdb_close (handle) -> 0 ---- + // Releases the CDB data for the given handle. linker.func_wrap( "dns", "cdb_close", - |_caller: Caller<'_, HostState>, _handle: i32| -> i32 { 0 } + |mut caller: Caller<'_, HostState>, handle: i32| -> i32 { + caller.data_mut().cdb_handles.remove(&handle); + 0 + } )?; Ok(()) --- a/lib/jerboa/wasm/scheme-runtime.sls +++ b/lib/jerboa/wasm/scheme-runtime.sls @@ -29,6 +29,7 @@ runtime-equality-forms runtime-conversion-forms runtime-io-forms + runtime-display-forms runtime-result-forms runtime-closure-forms runtime-closure-type-forms @@ -530,6 +531,69 @@ )) ;; ================================================================ + ;; Display output via log_message host import + ;; ================================================================ + ;; + ;; These forms use the "dns" module's log_message import (level 2 = INFO). + ;; They are only included by slang->wasm-forms when DNS host imports are + ;; present — do NOT add to runtime-all-forms. + ;; + ;; Scratch memory layout (in MEM-IO-BASE = 4096): + ;; 4096-4107: digit buffer for number→string (12 bytes; max -2147483648) + + (define runtime-display-forms + '( + ;; Write len raw bytes starting at ptr to log (level 2 = INFO). + (define (scheme-write-bytes ptr len) + (log_message 2 ptr len)) + + ;; Write the UTF-8 content of a WASM string object to log. + ;; String layout: [header(4)] [byte-len(4)] [utf8-bytes...] + (define (scheme-write-string s) + (scheme-write-bytes (+ s 8) (string-length-bytes s))) + + ;; Write a raw i32 as decimal ASCII digits to log. + ;; Uses scratch at 4096-4107 (first 12 bytes of MEM-IO-BASE). + (define (scheme-write-fixnum-raw n) + (if (= n 0) + (begin + (i32.store8 4107 48) ;; '0' + (scheme-write-bytes 4107 1)) + (let ([pos 4107] + [abs-n (if (< n 0) (- 0 n) n)] + [cnt 0]) + (let ([rem abs-n]) + (while (> rem 0) + (i32.store8 pos (+ 48 (remainder rem 10))) + (set! rem (quotient rem 10)) + (set! pos (- pos 1)) + (set! cnt (+ cnt 1)))) + (when (< n 0) + (i32.store8 pos 45) ;; '-' + (set! pos (- pos 1)) + (set! cnt (+ cnt 1))) + (scheme-write-bytes (+ pos 1) cnt)))) + + ;; Display a Scheme value to log (no explicit newline; + ;; each log_message call becomes one log line on the host side). + (define (scheme-display val) + (if (is-string val) + (scheme-write-string val) + (if (is-number val) + (scheme-write-fixnum-raw (untag-fixnum val)) + 0))) + + ;; Display a Scheme value (equivalent to scheme-display; each call + ;; is its own log line, so the newline is implicit). + (define (scheme-displayln val) + (scheme-display val)) + + ;; Log a blank line (empty message). + (define (scheme-newline) + (log_message 2 4096 0)) + )) + + ;; ================================================================ ;; Result type operations (ok/err) ;; ================================================================ --- a/lib/std/secure/wasm-target.sls +++ b/lib/std/secure/wasm-target.sls @@ -666,6 +666,33 @@ (begin ,@(map lower-expr catch-body))))) try-body)))] + ;; ---- Output / display forms ---- + + ;; (displayln x) → scheme-displayln (one log line per call) + ;; (displayln) → scheme-newline (blank log line) + [(displayln) + (if (null? args) + `(scheme-newline) + `(scheme-displayln ,(lower-expr (car args))))] + + ;; (display x) → scheme-display + [(display) + (if (null? args) + `(scheme-newline) + `(scheme-display ,(lower-expr (car args))))] + + ;; (newline) → scheme-newline + [(newline) + `(scheme-newline)] + + ;; (format str) → static string; (format str args...) → ignored for now + ;; MVP: if called with a literal string and no interpolation args, + ;; return the string; otherwise return first arg unchanged. + [(format) + (if (and (string? (car args)) (null? (cdr args))) + `(string-from-static ,(string->utf8 (car args))) + (lower-expr (car args)))] + ;; (assert! expr) or (assert! expr "message") [(assert!) (let ([test (lower-expr (car args))]) @@ -990,6 +1017,9 @@ (closure-runtime-forms) '()) + ;; 4c. Display runtime (requires log_message from dns imports) + (display-runtime-forms) + ;; 5. Function table (for closures via call_indirect) (if has-closures '((define-table 64 256)) @@ -1126,6 +1156,19 @@ #:handle-all)]) (if (pair? rt) rt '()))) + ;; Load display runtime forms from scheme-runtime. + ;; Only included in full Slang pipeline (requires log_message from dns imports). + (define (display-runtime-forms) + (let ([rt (with-exception-handler + (lambda (e) '()) + (lambda () + (eval '(begin + (import (jerboa wasm scheme-runtime)) + runtime-display-forms) + (environment '(chezscheme) '(jerboa wasm scheme-runtime)))) + #:handle-all)]) + (if (pair? rt) rt '()))) + ;; Check if any form references closures (define (has-closures? forms) (let ([found #f])