Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
444 changes: 437 additions & 7 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"nix-wasm-plugin-yaml",
"nix-wasm-plugin-mandelbrot",
"nix-wasm-plugin-test",
"nix-wasm-plugin-quickjs",
"nix-wasm-plugin-grep",
]
resolver = "2"
Expand Down
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 107 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
inputs = {
nixpkgs.follows = "nix/nixpkgs";
flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/*.tar.gz";
nix.url = "github:DeterminateSystems/nix-src/wasm";
nix.url = "github:DeterminateSystems/nix-src/main";
};

outputs = { self, ... }@inputs:
Expand All @@ -20,6 +20,105 @@
packages = forAllSystems ({ pkgs, system }: rec {
default = nix-wasm-plugins;

nix-wasm-plugin-quickjs = with pkgs;
let
quickjsCargoToml = builtins.fromTOML (builtins.readFile ./nix-wasm-plugin-quickjs/Cargo.toml);
rustPackages = pkgs.rustPackages_1_89;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the flake.nix file content to understand the setup
head -50 flake.nix

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 2162


🏁 Script executed:

# Search for rustPackages references and Rust version patterns in nix files
rg "rustPackages|rust.*1\.89" --type nix -B2 -A2

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 1142


🏁 Script executed:

# Check if there are any inputs or nixpkgs pinning
rg "inputs\.|nixpkgs" --type nix -B1 -A1

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 731


🏁 Script executed:

# Check if flake.lock exists to see the pinned nixpkgs version
cat flake.lock 2>/dev/null | head -100 || echo "flake.lock not found"

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 3237


🏁 Script executed:

# Check if there are any other references to Rust version selection or error handling
rg "rustPackages|rustc|rust.* 1\." --type nix -A3 -B3

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 2644


🏁 Script executed:

# Check if there's any fallback or version selection logic
rg "if.*then|else|or " --type nix | head -20

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 510


Hard-coded Rust version dependency is rigid; ensure it remains compatible with nixpkgs updates.

The reference to rustPackages_1_89 depends on the pinned nixpkgs providing this attribute. While the current flake.lock (Jan 2025) should include Rust 1.89, this creates a maintenance risk if nixpkgs is updated to an older version or if the attribute is removed in future nixpkgs releases. Consider adding version flexibility or explicit handling for version availability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` at line 26, The flake currently pins a hard-coded attribute
rustPackages_1_89 which may not exist in future nixpkgs; update the rustPackages
binding so it first uses pkgs.rustPackages_1_89 when available and otherwise
falls back to a stable/default like pkgs.rustPackages (or use
pkgs.rustChannels.stable.*) to avoid breakage. Locate the rustPackages =
pkgs.rustPackages_1_89 line and replace it with a conditional/fallback that
checks pkgs ? rustPackages_1_89 and selects the alternative when missing;
alternatively, switch to using pkgs.rustChannels or an explicit rust toolchain
provider so the flake remains robust against nixpkgs attribute changes.

rustPlatform = rustPackages.rustPlatform;
rustSysroot = runCommand "rust-sysroot" { } ''
mkdir -p $out/lib/rustlib
cp -r ${rustPackages."rustc-unwrapped"}/lib/rustlib/* $out/lib/rustlib/
mkdir -p $out/lib/rustlib/src
ln -s ${rustPlatform.rustcSrc} $out/lib/rustlib/src/rust
'';
rustcWithSysroot = runCommand "rustc-with-sysroot" { } ''
mkdir -p $out/bin
cat > $out/bin/rustc <<'EOF'
#!/bin/sh
exec ${rustPackages.rustc}/bin/rustc --sysroot ${rustSysroot} "$@"
EOF
chmod +x $out/bin/rustc
'';
wasiCc = pkgs.pkgsCross.wasi32.stdenv.cc;
wasiLibc = pkgs.pkgsCross.wasi32.wasilibc;
wasiLibcDev = wasiLibc.dev;
wasiSysroot = runCommand "wasi-sysroot" { } ''
mkdir -p $out/include $out/lib/wasm32-wasip1
cp -R ${wasiLibcDev}/include/* $out/include/
cp -R ${wasiLibc}/lib/* $out/lib/
cp -R ${wasiLibc}/lib/* $out/lib/wasm32-wasip1/
'';
wasiSdk = runCommand "wasi-sdk-compat" { } ''
mkdir -p $out/bin $out/lib/clang/19 $out/share

ln -s ${wasiCc}/bin/wasm32-unknown-wasi-clang $out/bin/clang
ln -s ${wasiCc}/bin/wasm32-unknown-wasi-clang++ $out/bin/clang++
ln -s ${wasiCc}/bin/wasm32-unknown-wasi-ar $out/bin/ar
ln -s ${wasiCc}/bin/wasm32-unknown-wasi-ld.lld $out/bin/ld.lld

ln -s ${wasiCc}/resource-root/include $out/lib/clang/19/include
ln -s ${wasiSysroot} $out/share/wasi-sysroot
'';
workspaceVendor = rustPlatform.fetchCargoVendor {
src = self;
hash = "sha256-c2jpj5YfRKIiIAwry0dOoNzAqW6YUdnHWsCe61t/New=";
};
stdlibVendor = rustPlatform.fetchCargoVendor {
src = rustPlatform.rustcSrc;
cargoRoot = "library";
hash = "sha256-XD+1wJ7GfnJG4qyulIdZum7VV4rtIoQRM+L0xXUHjXA=";
};
cargoVendor = runCommand "cargo-vendor-merged" { } ''
mkdir -p $out
cp -R ${workspaceVendor}/* $out/
mkdir -p $out/.cargo
cp -R ${workspaceVendor}/.cargo/* $out/.cargo/
chmod -R u+w $out
cp -R ${stdlibVendor}/* $out/
cp ${workspaceVendor}/Cargo.lock $out/Cargo.lock
cp ${workspaceVendor}/.cargo/config.toml $out/.cargo/config.toml
'';
in rustPlatform.buildRustPackage {
pname = quickjsCargoToml.package.name;
version = quickjsCargoToml.package.version;

cargoLock.lockFile = ./Cargo.lock;
cargoDeps = cargoVendor;

src = self;

buildPhase = ''
RUSTFLAGS="-L ${wasiSdk}/share/wasi-sysroot/lib/wasm32-wasip1" \
cargo build --release -p nix-wasm-plugin-quickjs \
--target wasm32-wasip1 -Z build-std=std,panic_abort
'';

installPhase = ''
mkdir -p $out
for i in target/wasm32-wasip1/release/*.wasm; do
wasm-opt -O3 --enable-bulk-memory --enable-nontrapping-float-to-int --enable-simd -o "$out/$(basename "$i")" "$i"
done
'';

nativeBuildInputs = [
rustPackages.rustc.llvmPackages.lld
binaryen
llvmPackages.clang
llvmPackages.libclang
];

WASI_SDK = "${wasiSdk}";
LIBCLANG_PATH = "${llvmPackages.libclang.lib}/lib";
RUSTC = "${rustcWithSysroot}/bin/rustc";
CC_wasm32_wasip1 = "${wasiSdk}/bin/clang";
AR_wasm32_wasip1 = "${wasiSdk}/bin/ar";
CFLAGS_wasm32_wasip1 = "--sysroot=${wasiSdk}/share/wasi-sysroot -isystem ${wasiSdk}/lib/clang/19/include";
BINDGEN_EXTRA_CLANG_ARGS_wasm32_wasip1 = "-fvisibility=default --sysroot=${wasiSdk}/share/wasi-sysroot -isystem ${wasiSdk}/lib/clang/19/include -resource-dir ${wasiSdk}/lib/clang/19";
CARGO_TARGET_WASM32_WASIP1_LINKER = "${wasiSdk}/bin/ld.lld";
RUSTC_BOOTSTRAP = "1";
doCheck = false;
};

nix-wasm-plugins = with pkgs; rustPlatform.buildRustPackage {
pname = cargoToml.package.name;
version = cargoToml.package.version;
Expand All @@ -29,13 +128,17 @@
src = self;

CARGO_BUILD_TARGET = "wasm32-unknown-unknown";
buildPhase = "cargo build --release";
buildPhase = "cargo build --release --workspace --exclude nix-wasm-plugin-quickjs";

checkPhase = ''
mkdir -p plugins
cp target/wasm32-unknown-unknown/release/*.wasm plugins/
cp ${nix-wasm-plugin-quickjs}/*.wasm plugins/

for i in nix-wasm-plugin-*/tests/*.nix; do
echo "running test $i..."
base="$(dirname $i)/$(basename $i .nix)"
nix eval --store dummy:// --offline --json --show-trace -I plugins=target/wasm32-unknown-unknown/release --impure --eval-cores 0 --file "$i" > "$base.out"
nix eval --store dummy:// --offline --json --show-trace -I plugins=plugins --impure --eval-cores 0 --file "$i" > "$base.out"
cmp "$base.exp" "$base.out"
done
'';
Expand All @@ -45,6 +148,7 @@
for i in target/wasm32-unknown-unknown/release/*.wasm; do
wasm-opt -O3 -o "$out/$(basename "$i")" "$i"
done
cp ${nix-wasm-plugin-quickjs}/*.wasm $out/
'';

nativeBuildInputs = [
Expand Down
11 changes: 11 additions & 0 deletions nix-wasm-plugin-quickjs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "nix-wasm-plugin-quickjs"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
nix-wasm-rust = { path = "../nix-wasm-rust" }
rquickjs = { version = "0.11.0", features = ["bindgen"] }
82 changes: 82 additions & 0 deletions nix-wasm-plugin-quickjs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use nix_wasm_rust::{nix_wasm_init_v1, warn, wasi_arg, Value};
use rquickjs::{Array, Context, Object, Runtime, Value as JsValue};
use std::string::String as StdString;

fn fail(context: &str, err: impl std::fmt::Display) -> ! {
warn!("quickjs {context} failed: {err}");
panic!("quickjs {context} failed: {err}");
}

fn js_value_to_nix(value: JsValue) -> Value {
if value.is_null() || value.is_undefined() {
return Value::make_null();
}
if let Some(b) = value.as_bool() {
return Value::make_bool(b);
}
if let Some(i) = value.as_int() {
return Value::make_int(i as i64);
}
if let Some(f) = value.as_float() {
return Value::make_float(f);
}
if let Some(js_str) = value.as_string() {
let s = js_str.to_string().unwrap_or_else(|err| fail("string conversion", err));
return Value::make_string(&s);
}
if value.is_array() {
let array: Array = value
.clone()
.into_array()
.unwrap_or_else(|| fail("array conversion", "value is not an array"));
let mut items = Vec::new();
for entry in array.into_iter() {
let entry = entry.unwrap_or_else(|err| fail("array iteration", err));
items.push(js_value_to_nix(entry));
}
return Value::make_list(&items);
}
if value.is_object() {
let object: Object = value
.into_object()
.unwrap_or_else(|| fail("object conversion", "value is not an object"));
let mut entries: Vec<(StdString, Value)> = Vec::new();
for entry in object.props::<StdString, JsValue>() {
let (key, value) = entry.unwrap_or_else(|err| fail("object iteration", err));
entries.push((key, js_value_to_nix(value)));
}
let attrs: Vec<(&str, Value)> = entries
.iter()
.map(|(key, value)| (key.as_str(), *value))
.collect();
return Value::make_attrset(&attrs);
}
Comment on lines +10 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add recursion guard for nested/cyclic JS values.

Lines [10-53] recurse without depth/cycle limits; deeply nested or self-referential objects can overflow the stack instead of failing cleanly.

Proposed hardening patch
-fn js_value_to_nix(value: JsValue) -> Value {
+const MAX_CONVERSION_DEPTH: usize = 256;
+
+fn js_value_to_nix(value: JsValue) -> Value {
+    js_value_to_nix_with_depth(value, 0)
+}
+
+fn js_value_to_nix_with_depth(value: JsValue, depth: usize) -> Value {
+    if depth > MAX_CONVERSION_DEPTH {
+        fail("value conversion", "maximum nesting depth exceeded");
+    }
     if value.is_null() || value.is_undefined() {
         return Value::make_null();
     }
@@
     if value.is_array() {
         let array: Array = value
             .clone()
             .into_array()
             .unwrap_or_else(|| fail("array conversion", "value is not an array"));
         let mut items = Vec::new();
         for entry in array.into_iter() {
             let entry = entry.unwrap_or_else(|err| fail("array iteration", err));
-            items.push(js_value_to_nix(entry));
+            items.push(js_value_to_nix_with_depth(entry, depth + 1));
         }
         return Value::make_list(&items);
     }
@@
         let mut entries: Vec<(StdString, Value)> = Vec::new();
         for entry in object.props::<StdString, JsValue>() {
             let (key, value) = entry.unwrap_or_else(|err| fail("object iteration", err));
-            entries.push((key, js_value_to_nix(value)));
+            entries.push((key, js_value_to_nix_with_depth(value, depth + 1)));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-wasm-plugin-quickjs/src/lib.rs` around lines 10 - 53, The js_value_to_nix
function currently recurses unbounded and can overflow on deeply nested or
cyclic JS values; modify js_value_to_nix to accept a recursion depth counter
(e.g., depth: usize) and a cycle-detection context (e.g., a mutable HashSet or
pointer-ID set) and bail with a clear error when depth exceeds a safe max or
when a JS object/array identity is seen twice; update all recursive calls
(including those in the array handling loop and the object props loop) to pass
depth+1 and the same context, and ensure public call sites initialize depth=0
and an empty context before invoking js_value_to_nix so Value::make_list and
Value::make_attrset only receive fully validated, non-cyclic conversions.


warn!("quickjs value type not supported: {:?}", value.type_of());
panic!("quickjs value type not supported: {:?}", value.type_of());
}

fn eval_impl(arg: Value) -> Value {
let code = arg.get_string();

let runtime = Runtime::new().unwrap_or_else(|err| fail("runtime init", err));
let context = Context::full(&runtime).unwrap_or_else(|err| fail("context init", err));

context.with(|ctx| {
let value: JsValue = ctx.eval(code).unwrap_or_else(|err| fail("eval", err));
js_value_to_nix(value)
})
}

#[no_mangle]
pub extern "C" fn eval(arg: Value) -> Value {
nix_wasm_init_v1();
eval_impl(arg)
}

#[no_mangle]
pub extern "C" fn _start() {
nix_wasm_init_v1();
let result = eval_impl(wasi_arg());
result.return_to_nix();
}
1 change: 1 addition & 0 deletions nix-wasm-plugin-quickjs/tests/eval.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"arr":[1,"two",false],"bool":true,"float":1.5,"nil":null,"num":1,"obj":{"nested":3},"str":"hello","undef":null}
12 changes: 12 additions & 0 deletions nix-wasm-plugin-quickjs/tests/eval.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
builtins.wasi <plugins/nix_wasm_plugin_quickjs.wasm> ''
({
num: 1,
float: 1.5,
str: "hello",
bool: true,
nil: null,
undef: undefined,
arr: [1, "two", false],
obj: { nested: 3 }
})
''
25 changes: 25 additions & 0 deletions nix-wasm-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ pub struct Value(ValueId);

type ValueId = u32;

pub fn wasi_arg() -> Value {
let arg = std::env::args()
.nth(1)
.unwrap_or_else(|| panic("missing WASI argument"));
let value_id = arg.parse::<ValueId>().unwrap_or_else(|err| {
panic(&format!("invalid WASI argument '{arg}': {err}"))
});
Value::from_raw(value_id)
}

#[repr(C)]
pub enum Type {
Int = 1,
Expand All @@ -50,6 +60,21 @@ pub enum Type {
}

impl Value {
pub const fn from_raw(value: ValueId) -> Value {
Value(value)
}

pub const fn id(self) -> ValueId {
self.0
}

pub fn return_to_nix(self) -> ! {
extern "C" {
fn return_to_nix(value: ValueId) -> !;
}
unsafe { return_to_nix(self.0) }
}

pub fn get_type(&self) -> Type {
extern "C" {
fn get_type(value: ValueId) -> Type;
Expand Down