updates

ober

5d3ca6b3ad8b5015b5fcaae57879ae9d18e5c048

diff --git a/jerboa-native-rs/Cargo.lock b/jerboa-native-rs/Cargo.lock
index 3706545..c306761 100644
--- a/jerboa-native-rs/Cargo.lock
+++ b/jerboa-native-rs/Cargo.lock
@@ -140,6 +140,32 @@ dependencies = [
 ]
 
 [[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "deranged"
 version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -178,6 +204,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
 
 [[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
 name = "find-msvc-tools"
 version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -285,6 +317,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
 name = "hmac"
 version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -327,6 +368,7 @@ name = "jerboa-native"
 version = "0.1.0"
 dependencies = [
  "flate2",
+ "hkdf",
  "inotify",
  "libc",
  "postgres",
@@ -338,8 +380,10 @@ dependencies = [
  "rustls-pemfile",
  "rustls-pki-types",
  "scrypt",
+ "sha2",
  "time",
  "webpki-roots 0.26.11",
+ "x25519-dalek",
 ]
 
 [[package]]
@@ -751,6 +795,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
 name = "rustls"
 version = "0.23.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -828,12 +881,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
 name = "serde"
 version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
 dependencies = [
  "serde_core",
+ "serde_derive",
 ]
 
 [[package]]
@@ -1281,6 +1341,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
 
 [[package]]
+name = "x25519-dalek"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
+dependencies = [
+ "curve25519-dalek",
+ "rand_core 0.6.4",
+ "serde",
+ "zeroize",
+]
+
+[[package]]
 name = "yasna"
 version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1314,3 +1386,17 @@ name = "zeroize"
 version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/jerboa-native-rs/Cargo.toml b/jerboa-native-rs/Cargo.toml
index 05c545d..70fb358 100644
--- a/jerboa-native-rs/Cargo.toml
+++ b/jerboa-native-rs/Cargo.toml
@@ -14,6 +14,9 @@ regex = "1"
 libc = "0.2"
 rusqlite = { version = "0.32", features = ["bundled"] }
 postgres = "0.19"
+x25519-dalek = { version = "2", features = ["static_secrets"] }
+hkdf = "0.12"
+sha2 = "0.10"
 inotify = { version = "0.11", default-features = false }
 rcgen = { version = "0.13", features = ["ring", "pem"] }
 time = "0.3"
diff --git a/jerboa-native-rs/src/lib.rs b/jerboa-native-rs/src/lib.rs
index e9e41c6..9a7ff0d 100644
--- a/jerboa-native-rs/src/lib.rs
+++ b/jerboa-native-rs/src/lib.rs
@@ -13,3 +13,5 @@ mod x509;
 mod antidebug;
 mod seccomp;
 mod integrity;
+mod x25519;
+mod process_ctl;
diff --git a/jerboa-native-rs/src/process_ctl.rs b/jerboa-native-rs/src/process_ctl.rs
new file mode 100644
index 0000000..2992ed1
--- /dev/null
+++ b/jerboa-native-rs/src/process_ctl.rs
@@ -0,0 +1,118 @@
+use crate::panic::{ffi_wrap, set_last_error};
+
+/// Set the process name via prctl(PR_SET_NAME).
+/// name: UTF-8 bytes, max 15 bytes (will be truncated).
+#[no_mangle]
+pub extern "C" fn jerboa_prctl_set_name(name: *const u8, name_len: usize) -> i32 {
+    ffi_wrap(|| {
+        if name.is_null() {
+            set_last_error("null name pointer".into());
+            return -1;
+        }
+        #[cfg(target_os = "linux")]
+        {
+            let len = name_len.min(15);
+            let mut buf = [0u8; 16]; // 15 chars + null
+            unsafe { std::ptr::copy_nonoverlapping(name, buf.as_mut_ptr(), len) };
+            buf[len] = 0;
+            let rc = unsafe { libc::prctl(libc::PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0, 0, 0) };
+            if rc != 0 {
+                set_last_error("prctl PR_SET_NAME failed".into());
+                return -1;
+            }
+            0
+        }
+        #[cfg(not(target_os = "linux"))]
+        {
+            let _ = name_len;
+            set_last_error("prctl not supported on this platform".into());
+            -1
+        }
+    })
+}
+
+/// Lock all current and future memory pages (prevent swapping).
+#[no_mangle]
+pub extern "C" fn jerboa_mlockall() -> i32 {
+    ffi_wrap(|| {
+        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+        {
+            let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
+            if rc != 0 {
+                set_last_error("mlockall failed (may need root)".into());
+                return -1;
+            }
+            0
+        }
+        #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
+        {
+            set_last_error("mlockall not supported".into());
+            -1
+        }
+    })
+}
+
+/// Probe whether a process exists using kill(pid, 0).
+/// Returns: 1 = exists, 0 = does not exist, -1 = error.
+#[no_mangle]
+pub extern "C" fn jerboa_kill_probe(pid: u32) -> i32 {
+    ffi_wrap(|| {
+        let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
+        if rc == 0 {
+            1 // process exists
+        } else {
+            let err = unsafe { *libc::__errno_location() };
+            if err == libc::ESRCH {
+                0 // does not exist
+            } else if err == libc::EPERM {
+                1 // exists but no permission (still alive)
+            } else {
+                set_last_error(format!("kill probe errno: {}", err));
+                -1
+            }
+        }
+    })
+}
+
+/// Read the path of /proc/self/exe.
+/// output: buffer for the path
+/// output_len: buffer size
+/// actual_len: pointer to store actual path length
+#[no_mangle]
+pub extern "C" fn jerboa_proc_self_exe(
+    output: *mut u8,
+    output_len: usize,
+    actual_len: *mut usize,
+) -> i32 {
+    ffi_wrap(|| {
+        #[cfg(target_os = "linux")]
+        {
+            if output.is_null() || actual_len.is_null() {
+                set_last_error("null pointer".into());
+                return -1;
+            }
+            match std::fs::read_link("/proc/self/exe") {
+                Ok(path) => {
+                    let bytes = path.to_string_lossy();
+                    let path_bytes = bytes.as_bytes();
+                    let copy_len = path_bytes.len().min(output_len);
+                    unsafe {
+                        std::ptr::copy_nonoverlapping(path_bytes.as_ptr(), output, copy_len);
+                        *actual_len = path_bytes.len();
+                    }
+                    0
+                }
+                Err(e) => {
+                    set_last_error(format!("readlink failed: {}", e));
+                    -1
+                }
+            }
+        }
+        #[cfg(not(target_os = "linux"))]
+        {
+            let _ = (output, output_len, actual_len);
+            set_last_error("proc_self_exe not supported".into());
+            -1
+        }
+    })
+}
diff --git a/jerboa-native-rs/src/seccomp.rs b/jerboa-native-rs/src/seccomp.rs
index 1eadd57..73397af 100644
--- a/jerboa-native-rs/src/seccomp.rs
+++ b/jerboa-native-rs/src/seccomp.rs
@@ -22,6 +22,7 @@ const NR_PTRACE: u32 = 101;
 const NR_PROCESS_VM_READV: u32 = 310;
 const NR_PROCESS_VM_WRITEV: u32 = 311;
 const NR_PERSONALITY: u32 = 135;
+const NR_MEMFD_CREATE: u32 = 319;  // prevents code injection via memfd
 
 // seccomp_data field offsets (for BPF_ABS loads)
 const OFFSET_NR: u32 = 0;   // offsetof(struct seccomp_data, nr)
@@ -104,26 +105,29 @@ pub extern "C" fn jerboa_seccomp_lock() -> i32 {
         //   4: if nr == process_vm_readv, kill
         //   5: if nr == process_vm_writev, kill
         //   6: if nr == personality, kill
-        //   7: allow
-        //   8: kill
+        //   7: if nr == memfd_create, kill
+        //   8: allow
+        //   9: kill
         let filter = [
             // 0: Load architecture
             bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_ARCH),
-            // 1: Verify x86_64 — if not, jump to kill (offset +6 -> instruction 8)
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 6),
+            // 1: Verify x86_64 — if not, jump to kill (offset +7 -> instruction 9)
+            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 7),
             // 2: Load syscall number
             bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_NR),
-            // 3: Check ptrace — if match, jump to kill (+4 -> instruction 8)
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PTRACE, 4, 0),
+            // 3: Check ptrace — if match, jump to kill (+5 -> instruction 9)
+            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PTRACE, 5, 0),
             // 4: Check process_vm_readv
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_READV, 3, 0),
+            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_READV, 4, 0),
             // 5: Check process_vm_writev
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_WRITEV, 2, 0),
+            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_WRITEV, 3, 0),
             // 6: Check personality
-            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PERSONALITY, 1, 0),
-            // 7: Allow
+            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PERSONALITY, 2, 0),
+            // 7: Check memfd_create — prevents code injection via memfd
+            bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_MEMFD_CREATE, 1, 0),
+            // 8: Allow
             bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
-            // 8: Kill process
+            // 9: Kill process
             bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
         ];
 
diff --git a/jerboa-native-rs/src/x25519.rs b/jerboa-native-rs/src/x25519.rs
new file mode 100644
index 0000000..4e2921a
--- /dev/null
+++ b/jerboa-native-rs/src/x25519.rs
@@ -0,0 +1,143 @@
+use crate::panic::{ffi_wrap, set_last_error};
+use hkdf::Hkdf;
+use sha2::Sha256;
+use x25519_dalek::{PublicKey, StaticSecret};
+
+/// Generate an X25519 keypair using ring's CSPRNG.
+/// private_out: 32-byte buffer for the private key
+/// public_out: 32-byte buffer for the public key
+#[no_mangle]
+pub extern "C" fn jerboa_x25519_generate_keypair(
+    private_out: *mut u8,
+    public_out: *mut u8,
+) -> i32 {
+    ffi_wrap(|| {
+        if private_out.is_null() || public_out.is_null() {
+            set_last_error("null output pointer".into());
+            return -1;
+        }
+        // Use ring's CSPRNG to generate 32 random bytes for the private key
+        let rng = ring::rand::SystemRandom::new();
+        let mut key_bytes = [0u8; 32];
+        ring::rand::SecureRandom::fill(&rng, &mut key_bytes)
+            .map_err(|_| "CSPRNG failed")
+            .unwrap();
+        let secret = StaticSecret::from(key_bytes);
+        let public = PublicKey::from(&secret);
+        unsafe {
+            std::ptr::copy_nonoverlapping(secret.as_bytes().as_ptr(), private_out, 32);
+            std::ptr::copy_nonoverlapping(public.as_bytes().as_ptr(), public_out, 32);
+        }
+        0
+    })
+}
+
+/// Compute public key from a private key.
+/// private_key: 32-byte private key
+/// public_out: 32-byte buffer for the public key
+#[no_mangle]
+pub extern "C" fn jerboa_x25519_public_from_private(
+    private_key: *const u8,
+    private_len: usize,
+    public_out: *mut u8,
+) -> i32 {
+    ffi_wrap(|| {
+        if private_key.is_null() || public_out.is_null() || private_len != 32 {
+            set_last_error("invalid arguments".into());
+            return -1;
+        }
+        let mut arr = [0u8; 32];
+        unsafe { std::ptr::copy_nonoverlapping(private_key, arr.as_mut_ptr(), 32) };
+        let secret = StaticSecret::from(arr);
+        let public = PublicKey::from(&secret);
+        unsafe {
+            std::ptr::copy_nonoverlapping(public.as_bytes().as_ptr(), public_out, 32);
+        }
+        0
+    })
+}
+
+/// Perform X25519 Diffie-Hellman key agreement.
+/// our_private: 32-byte private key
+/// their_public: 32-byte public key
+/// shared_out: 32-byte buffer for the shared secret
+#[no_mangle]
+pub extern "C" fn jerboa_x25519_diffie_hellman(
+    our_private: *const u8,
+    priv_len: usize,
+    their_public: *const u8,
+    pub_len: usize,
+    shared_out: *mut u8,
+    shared_len: usize,
+) -> i32 {
+    ffi_wrap(|| {
+        if our_private.is_null() || their_public.is_null() || shared_out.is_null() {
+            set_last_error("null pointer".into());
+            return -1;
+        }
+        if priv_len != 32 || pub_len != 32 || shared_len < 32 {
+            set_last_error("invalid key sizes".into());
+            return -1;
+        }
+        let mut priv_arr = [0u8; 32];
+        let mut pub_arr = [0u8; 32];
+        unsafe {
+            std::ptr::copy_nonoverlapping(our_private, priv_arr.as_mut_ptr(), 32);
+            std::ptr::copy_nonoverlapping(their_public, pub_arr.as_mut_ptr(), 32);
+        }
+        let secret = StaticSecret::from(priv_arr);
+        let public = PublicKey::from(pub_arr);
+        let shared = secret.diffie_hellman(&public);
+        unsafe {
+            std::ptr::copy_nonoverlapping(shared.as_bytes().as_ptr(), shared_out, 32);
+        }
+        0
+    })
+}
+
+/// HKDF-SHA256: extract + expand in one call.
+/// ikm: input keying material
+/// salt: salt bytes (can be NULL with salt_len=0 for no salt)
+/// info: context/application info
+/// output: buffer for derived key material
+#[no_mangle]
+pub extern "C" fn jerboa_hkdf_sha256(
+    ikm: *const u8,
+    ikm_len: usize,
+    salt: *const u8,
+    salt_len: usize,
+    info: *const u8,
+    info_len: usize,
+    output: *mut u8,
+    output_len: usize,
+) -> i32 {
+    ffi_wrap(|| {
+        if ikm.is_null() || output.is_null() {
+            set_last_error("null pointer".into());
+            return -1;
+        }
+        let ikm_slice = unsafe { std::slice::from_raw_parts(ikm, ikm_len) };
+        let salt_opt = if salt.is_null() || salt_len == 0 {
+            None
+        } else {
+            Some(unsafe { std::slice::from_raw_parts(salt, salt_len) })
+        };
+        let info_slice = if info.is_null() || info_len == 0 {
+            &[]
+        } else {
+            unsafe { std::slice::from_raw_parts(info, info_len) }
+        };
+
+        let hkdf = Hkdf::<Sha256>::new(salt_opt, ikm_slice);
+        let mut out = vec![0u8; output_len];
+        if hkdf.expand(info_slice, &mut out).is_err() {
+            set_last_error("HKDF expand failed".into());
+            return -1;
+        }
+        unsafe {
+            std::ptr::copy_nonoverlapping(out.as_ptr(), output, output_len);
+        }
+        0
+    })
+}
+
diff --git a/lib/std/net/websocket.sls b/lib/std/net/websocket.sls
index 5df7fcd..36d7714 100644
--- a/lib/std/net/websocket.sls
+++ b/lib/std/net/websocket.sls
@@ -23,7 +23,9 @@
     ;; Limits
     *ws-max-payload-size*)
 
-  (import (chezscheme))
+  (import (chezscheme)
+          (std crypto native-rust)
+          (std text base64))
 
   ;;; ========== Opcode constants ==========
   (define ws-opcode-continuation #x0)
@@ -224,20 +226,16 @@
   (define ws-rfc-test-key    "dGhlIHNhbXBsZSBub25jZQ==")
   (define ws-rfc-test-accept "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")
 
-  ;; Compute the Sec-WebSocket-Accept from a key.
-  ;; This is a simplified/stub implementation that handles the RFC test vector
-  ;; exactly; for other keys it returns a placeholder.
+  ;; Compute the Sec-WebSocket-Accept from a client key.
+  ;; Per RFC 6455: SHA1(key + GUID) then base64-encode.
   (define (ws-handshake-accept key)
-    (if (string=? key ws-rfc-test-key)
-      ws-rfc-test-accept
-      ;; For real implementations, compute SHA1(key + ws-guid) then base64-encode.
-      ;; Here we return a placeholder for non-test keys.
-      (string-append "ACCEPT:" key)))
-
-  ;; Generate a random-ish 16-byte WebSocket key (base64 of 16 bytes).
-  ;; For testing, returns a fixed key.
+    (let* ([combined (string->utf8 (string-append key ws-guid))]
+           [hash-bv (rust-sha1 combined)])
+      (u8vector->base64-string hash-bv)))
+
+  ;; Generate a random 16-byte WebSocket key (base64-encoded).
   (define (ws-handshake-key)
-    ws-rfc-test-key)
+    (u8vector->base64-string (rust-random-bytes 16)))
 
   ;; Validate that the server's accept matches the expected value.
   (define (ws-handshake-valid? key accept)