Phase 5/6/7B: DNS sockets, crypto random, and display lowering

ober

348a782cdbe2d296594f60c9e534e6ece197b31a

diff --git a/jerboa-native-rs/Cargo.lock b/jerboa-native-rs/Cargo.lock
index 6c55e63..df9318a 100644
--- 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",
diff --git a/jerboa-native-rs/Cargo.toml b/jerboa-native-rs/Cargo.toml
index 226ed80..cae4eef 100644
--- 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 }
diff --git a/jerboa-native-rs/src/wasm.rs b/jerboa-native-rs/src/wasm.rs
index c6dd61e..de017dc 100644
--- 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(())
diff --git a/lib/jerboa/wasm/scheme-runtime.sls b/lib/jerboa/wasm/scheme-runtime.sls
index 3c32e5e..cb1d449 100644
--- 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)
   ;; ================================================================
 
diff --git a/lib/std/secure/wasm-target.sls b/lib/std/secure/wasm-target.sls
index 6b50042..23cbabd 100644
--- 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])