Skip to content
Draft
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
38 changes: 38 additions & 0 deletions executor/src/runners/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,41 @@ pub enum InitAction {
action: Box<InitAction>,
},
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn runner_action_accepts_schema_annotation() {
let parsed = serde_json::from_str::<InitAction>(
r#"{
"$schema": "https://raw.githubusercontent.com/genlayerlabs/genvm/refs/heads/main/doc/schemas/runner.json",
"StartWasm": "file"
}"#,
);

assert!(
matches!(parsed, Ok(InitAction::StartWasm(ref path)) if path.as_ref() == "file"),
"runner.json permits the optional `$schema` annotation; got {parsed:?}"
);
}

#[test]
fn runner_action_rejects_unknown_payload_fields() {
let parsed = serde_json::from_str::<InitAction>(
r#"{
"MapFile": {
"file": "contract.py",
"to": "/contract.py",
"destination": "/silently-ignored.py"
}
}"#,
);

assert!(
parsed.is_err(),
"unknown runner action fields must be rejected instead of silently ignored; got {parsed:?}"
);
}
}
123 changes: 123 additions & 0 deletions executor/src/runners/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,126 @@ fn code_to_archive_from_text(code: bytes::Bytes) -> rt::errors::Result<super::Ar
bytes::Bytes::copy_from_slice(code_comment.as_bytes()),
))
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;

const BLOCK_SIZE: usize = 512;

fn ustar(name: &[u8], prefix: &[u8], contents: &[u8]) -> bytes::Bytes {
ustar_with_type(name, prefix, contents, b'0')
}

fn ustar_with_type(name: &[u8], prefix: &[u8], contents: &[u8], type_flag: u8) -> bytes::Bytes {
assert!(name.len() <= 100);
assert!(prefix.len() <= 155);

let mut header = [0u8; BLOCK_SIZE];
header[..name.len()].copy_from_slice(name);
header[100..108].copy_from_slice(b"0000644\0");
header[108..116].copy_from_slice(b"0000000\0");
header[116..124].copy_from_slice(b"0000000\0");
let size = format!("{:011o}\0", contents.len());
header[124..136].copy_from_slice(size.as_bytes());
header[136..148].copy_from_slice(b"00000000000\0");
header[148..156].fill(b' ');
header[156] = type_flag;
header[257..265].copy_from_slice(b"ustar\x0000".as_slice());
header[345..345 + prefix.len()].copy_from_slice(prefix);
let checksum: u32 = header.iter().map(|byte| *byte as u32).sum();
header[148..156].copy_from_slice(format!("{checksum:06o}\0 ").as_bytes());

let padded_contents = contents.len().div_ceil(BLOCK_SIZE) * BLOCK_SIZE;
let mut archive = Vec::with_capacity(BLOCK_SIZE + padded_contents + 2 * BLOCK_SIZE);
archive.extend_from_slice(&header);
archive.extend_from_slice(contents);
archive.resize(BLOCK_SIZE + padded_contents, 0);
archive.resize(BLOCK_SIZE + padded_contents + 2 * BLOCK_SIZE, 0);
archive.into()
}

#[test]
fn runtime_parser_accepts_ustar() {
let archive = parse(ustar(b"runner.json", b"", br#"{"StartWasm":"file"}"#)).unwrap();
assert_eq!(
archive.data.get("runner.json").unwrap().as_ref(),
br#"{"StartWasm":"file"}"#
);
}

#[test]
fn ustar_prefix_is_separated_from_name() {
let archive =
super::super::Archive::from_ustar(ustar(b"file", b"nested", b"value")).unwrap();
assert_eq!(
archive.data.keys().map(String::as_str).collect::<Vec<_>>(),
["nested/file"],
"USTAR prefix and name fields must be joined with a slash"
);
}

#[test]
fn full_width_ustar_name_is_not_truncated() {
let name = [b'x'; 100];
let archive = super::super::Archive::from_ustar(ustar(&name, b"", b"value")).unwrap();
assert_eq!(
archive.data.keys().next().unwrap().len(),
name.len(),
"a full-width USTAR name field must retain its final byte"
);
}

#[test]
fn ustar_directory_type_is_not_exposed_as_a_file() {
let archive =
super::super::Archive::from_ustar(ustar_with_type(b"nested", b"", b"", b'5')).unwrap();
assert!(
archive.data.is_empty(),
"USTAR type flag `5` denotes a directory; got entries {:?}",
archive.data.keys().collect::<Vec<_>>()
);
}

#[test]
fn ustar_rejects_a_header_with_a_bad_checksum() {
let mut archive = ustar(b"runner.json", b"", br#"{"StartWasm":"file"}"#).to_vec();
archive[0] = b'R';

assert!(
super::super::Archive::from_ustar(archive.into()).is_err(),
"a USTAR header modified without updating its checksum must be rejected"
);
}

#[test]
fn zip_rejects_stored_contents_with_a_bad_crc() {
let contents = b"payload whose CRC must be checked";
let mut cursor = std::io::Cursor::new(Vec::new());
{
let mut writer = zip::ZipWriter::new(&mut cursor);
writer
.start_file(
"payload",
zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored),
)
.unwrap();
writer.write_all(contents).unwrap();
writer.finish().unwrap();
}

let mut archive = cursor.into_inner();
let contents_offset = archive
.windows(contents.len())
.position(|window| window == contents)
.unwrap();
archive[contents_offset] ^= 1;

assert!(
parse(archive.into()).is_err(),
"stored ZIP contents modified without updating their CRC must be rejected"
);
}
}
129 changes: 129 additions & 0 deletions executor/src/wasi/preview1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1366,9 +1366,138 @@ fn write_bytes_capacity(

#[cfg(test)]
mod tests {
use super::*;
use rand_core::RngCore as _;
use sha3::Digest as _;

fn test_context() -> Context {
Context::new(
chrono::DateTime::from_timestamp(0, 0).unwrap(),
base::Config {
needs_error_fingerprint: false,
permissions: base::Permissions {
deterministic: true,
write_storage: false,
send_messages: false,
call_others: false,
spawn_nondet: false,
register_runners: false,
can_use_balance_for_message_fees: false,
},
execution: base::Execution {
state_mode: crate::public_abi::StorageType::Default,
topmost_runner_id: crate::runners::Id::Custom {
hash: Bytes32Hash::ZERO,
},
},
},
[0; 32],
)
}

#[test]
fn regression_parent_component_traverses_to_parent_directory() {
let mut context = test_context();
context
.map_file("/parent/child/file", bytes::Bytes::from_static(b"value"))
.unwrap();
let mut vfs = vfs::VFS::new(Vec::new(), rt::memlimiter::Limiter::new()).unwrap();
let context_vfs = ContextVFS {
vfs: &mut vfs,
context: &mut context,
};

let FilesTrie::Dir { children } = context_vfs.context.fs.as_ref() else {
panic!("root must be a directory");
};
let parent = children.get("parent").unwrap();
let FilesTrie::Dir { children } = parent.as_ref() else {
panic!("parent must be a directory");
};
let child = children.get("child").unwrap();
let mut path = vec!["parent".to_owned(), "child".to_owned()];

let resolved = context_vfs
.dir_fd_get_trie("..", child, &mut Some(&mut path))
.expect("`..` inside the preopened filesystem must resolve its parent");

assert!(std::ptr::eq(resolved, parent.as_ref()));
assert_eq!(path, ["parent"]);
}

#[test]
fn regression_mapped_dot_component_is_reachable() {
let mut context = test_context();
context
.map_file("/parent/./file", bytes::Bytes::from_static(b"value"))
.unwrap();

let FilesTrie::Dir { children } = context.fs.as_ref() else {
panic!("root must be a directory");
};
let parent = children.get("parent").unwrap();
let FilesTrie::Dir { children } = parent.as_ref() else {
panic!("parent must be a directory");
};

assert!(
children.contains_key("file"),
"mapped paths must normalize `.` like the WASI path resolver; got {:?}",
children.keys().collect::<Vec<_>>()
);
}

#[test]
fn directory_mapping_rejects_normalized_path_collisions() {
let archive = crate::runners::ArchiveCache::new(
symbol_table::GlobalSymbol::from("custom:path-collision"),
crate::runners::Archive {
data: BTreeMap::from([
(
"tree/a//file".to_owned(),
bytes::Bytes::from_static(b"first"),
),
(
"tree/a/file".to_owned(),
bytes::Bytes::from_static(b"second"),
),
]),
total_size: 11,
},
);
let limiter = rt::memlimiter::Limiter::new();
let mut context = test_context();

crate::rt::supervisor::actions::map_archive_file(
&mut context,
&limiter,
&archive,
"tree/",
"/mapped/",
)
.expect_err(
"distinct archive entries that normalize to one VFS path must not silently overwrite",
);
}

#[test]
fn mapping_a_file_over_a_directory_does_not_delete_the_subtree() {
let mut context = test_context();
context
.map_file(
"/mapped/child",
bytes::Bytes::from_static(b"child contents"),
)
.unwrap();

assert!(
context
.map_file("/mapped", bytes::Bytes::from_static(b"replacement"))
.is_err(),
"mapping a file over an existing directory must not silently delete its children"
);
}

/// Known-answer test for the deterministic `random_get` byte layout. The seed is a
/// fixed 32-byte value consumed as 8 little-endian `u32` words (matching
/// `Context::new`), and `fill_bytes` produces the exact stream below. This pins the
Expand Down
5 changes: 5 additions & 0 deletions runners/genlayer-py-std/tests/test_calldata_corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ def test_calldata_corpus_decode_roundtrip():
assert calldata.decode(bytes.fromhex(expected)) == value, (
f'roundtrip mismatch for {value!r}'
)


def test_memoryview_roundtrip_as_bytes():
value = memoryview(b'abc')
assert calldata.decode(calldata.encode(value)) == value.tobytes()
Loading