Skip to content
Open
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
22 changes: 16 additions & 6 deletions SECURITY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,13 @@ This repository is a component of [genvm-manager]; the canonical security policy

## Reporting a vulnerability

**Do not open a public issue.** Report privately via GitHub's
[private vulnerability reporting](https://github.com/genlayerlabs/genvm-executor/security/advisories/new),
or email code owners, kira@genlayerlabs.com for instance
**Before mainnet, report everything except remote code execution publicly** — open a
regular issue. Until there is value at stake, an open report gets triaged faster and is
useful to everyone reading along. RCE is the only exception; report it privately.

For remote code execution, **do not open a public issue** — report it via GitHub's
[private vulnerability reporting](https://github.com/genlayerlabs/genvm-manager/security/advisories/new)
on the [genvm-manager] repository.

Include a description, affected component/version, and a reproduction (a contract, calldata,
or test case) where possible. We aim to acknowledge within a few business days.
Expand All@@ -35,8 +39,14 @@ Issues are triaged by impact, highest first:
The following relationships are trusted. Hardening them is welcome, but a report that assumes
one side is hostile is not treated as a vulnerability:

- host and GenVM
- executor and manager
- the local disk and loopback in general
- Host and GenVM
- Executor and manager
- The local disk and loopback in general

The following inputs are untrusted, even when delivered through a trusted component:

- Intelligent Contract code and contract-controlled data, including calldata, messages, and persisted values
- Data originating from other validators, including leader results
- External content processed by modules, including HTTP responses, redirects, rendered pages, JavaScript, subresources, and model-provider responses

[genvm-manager]: https://github.com/genlayerlabs/genvm-manager
6 changes: 5 additions & 1 deletion docs/website/src/python-sdk/migration-guide.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,7 +91,11 @@ VM Error Codes
* - ``invalid_contract malformed_runner``
- ``invalid_contract runner malformed``

``malformed_entry`` is new, ``out_of receipt message``, ``out_of message_fee total``, ``out_of message_fee allocation_budget`` and ``fee no_matching_allocation`` gained ``internal``/``external`` variants, and ``ResultCode.INTERNAL_ERROR`` is gone. The ``memory_limiter_consts`` and ``top_limits`` tables were removed from ``public_abi``.
``malformed_entry`` is new, ``out_of receipt message``, ``out_of message_fee total``,
``out_of message_fee allocation_budget`` and ``fee no_matching_allocation``
gained ``internal``/``external`` variants, and ``ResultCode.INTERNAL_ERROR`` is
gone. The ``memory_limiter_consts`` and ``top_limits`` tables were removed from
``public_abi``.

Storage
~~~~~~~
Expand Down
5 changes: 4 additions & 1 deletion executor/codegen/data/internal-constants.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,10 @@
"runner_load_cost": 4096,
"vm_spawn_cost": 134217728,
"new_storage_page": 256,
"storage_page_inherited": 128
"storage_page_inherited": 128,
"execution_emission_base_size": 256,
"message_fee_rotation_element_size": 32,
"nondet_output_base_size": 32
}
},
{
Expand Down
8 changes: 8 additions & 0 deletions executor/crates/common/src/expr/evaluator.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -204,6 +204,14 @@ static BUILTINS: std::sync::LazyLock<std::collections::HashMap<&'static str, Arc
}),
}),
);
m.insert(
"internalError",
Arc::new(|arg: &Value| {
let msg = arg.as_str()?.to_string();

Err(EvalError::ScriptInternalError(msg))
}),
);
m.insert(
"vmError",
Arc::new(|arg: &Value| {
Expand Down
26 changes: 23 additions & 3 deletions executor/crates/common/src/expr/value.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,8 @@ impl std::error::Error for ParseError {}
pub enum EvalError {
#[error("script VM error: {0}")]
ScriptVMError(String),
#[error("script internal error: {0}")]
ScriptInternalError(String),

#[error("undefined variable `{0}`")]
UndefinedVariable(String),
Expand DownExpand Up@@ -169,9 +171,15 @@ pub enum Value {
#[derive(Clone)]
pub struct Thunk(Arc<Mutex<ThunkState>>);

enum ThunkStateFailure {
Generic(String),
VMError(String),
InternalError(String),
}

enum ThunkState {
Forced(Value),
Failed(String),
Failed(ThunkStateFailure),
Deferred(Box<dyn FnOnce() -> Result<Value, EvalError> + Send>),
InProgress,
}
Expand All@@ -194,7 +202,13 @@ impl Thunk {
return Ok(v);
}
ThunkState::Failed(msg) => {
let err = EvalError::AlreadyFailed(msg.clone());
let err = match &msg {
ThunkStateFailure::Generic(m) => EvalError::AlreadyFailed(m.clone()),
ThunkStateFailure::InternalError(m) => {
EvalError::ScriptInternalError(m.clone())
}
ThunkStateFailure::VMError(m) => EvalError::ScriptVMError(m.clone()),
};
*state = ThunkState::Failed(msg);
return Err(err);
}
Expand All@@ -213,7 +227,13 @@ impl Thunk {
// A failed computation is not retried either: later forces report that failure
*state = match &result {
Ok(v) => ThunkState::Forced(v.clone()),
Err(e) => ThunkState::Failed(e.to_string()),
Err(EvalError::ScriptVMError(msg)) => {
ThunkState::Failed(ThunkStateFailure::VMError(msg.clone()))
}
Err(EvalError::ScriptInternalError(msg)) => {
ThunkState::Failed(ThunkStateFailure::InternalError(msg.clone()))
}
Err(e) => ThunkState::Failed(ThunkStateFailure::Generic(e.to_string())),
};

result
Expand Down
3 changes: 3 additions & 0 deletions executor/crates/common/src/internal_constants.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ pub mod memory_limiter_consts {
pub const VM_SPAWN_COST: u32 = 134217728;
pub const NEW_STORAGE_PAGE: u32 = 256;
pub const STORAGE_PAGE_INHERITED: u32 = 128;
pub const EXECUTION_EMISSION_BASE_SIZE: u32 = 256;
pub const MESSAGE_FEE_ROTATION_ELEMENT_SIZE: u32 = 32;
pub const NONDET_OUTPUT_BASE_SIZE: u32 = 32;
}

pub mod top_limits {
Expand Down
63 changes: 34 additions & 29 deletions executor/crates/common/tests/fees_abi.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,11 +71,11 @@ fn internal_node(
#[test]
fn external_root_node_matches_exact_encoding() {
let recipient = [0x11u8; 20];
let encoded =
MessageAllocationNode::abi_encode(&[external_node(Some(recipient), None, 5, 7, 9, vec![])]);
let root = external_node(Some(recipient), None, 5, 7, 9, vec![]);
let encoded = root.abi_encode();

// `abi.encode(MessageAllocationNode[])` of a single external root node:
// array offset, length, element offset, then the 10-word element tuple
// `abi.encode(MessageAllocationNode[])`: array offset, length, element
// offset, then the 10-word element tuple
// (messageType=External, onAcceptance=false, parent=sentinel, recipient,
// callKey wildcard, budget, feeParams offset, feeParams len, gasLimit, maxGasPrice).
let expected = words(&[
Expand All@@ -86,12 +86,12 @@ fn external_root_node_matches_exact_encoding() {
U256::from(0), // onAcceptance = false
U256::MAX, // parentIndex = NODE_ROOT_SENTINEL
U256::from_big_endian(&recipient), // recipient (left-padded)
U256::from(0), // callKey = CALL_KEY_WILDCARD
U256::from(5), // budget
U256::from(0xE0), // feeParams offset (7 head words)
U256::from(64), // feeParams bytes length
U256::from(7), // gasLimit
U256::from(9), // maxGasPrice
U256::from_big_endian(&genvm_modules_interfaces::fees::CALL_KEY_WILDCARD.0),
U256::from(5), // budget
U256::from(0xE0), // feeParams offset (7 head words)
U256::from(64), // feeParams bytes length
U256::from(7), // gasLimit
U256::from(9), // maxGasPrice
]);

assert_eq!(encoded, expected);
Expand All@@ -101,25 +101,29 @@ fn external_root_node_matches_exact_encoding() {

#[test]
fn nested_internal_flattens_with_parent_pointers() {
// root (internal, accepted) with a single external child.
let child = external_node(Some([0x22u8; 20]), None, 1, 100, 200, vec![]);
let grandchild = external_node(Some([0x44u8; 20]), None, 1, 100, 200, vec![]);
let first_child = external_node(Some([0x22u8; 20]), None, 2, 100, 200, vec![grandchild]);
let second_child = external_node(Some([0x33u8; 20]), None, 3, 100, 200, vec![]);
let root = internal_node(
genvm_modules_interfaces::On::Decided,
10,
&[2, 3],
vec![child],
vec![first_child, second_child],
);

let encoded = MessageAllocationNode::abi_encode(&[root]);
let encoded = root.abi_encode();

assert_eq!(word(&encoded, 0), U256::from(0x20));
assert_eq!(word(&encoded, 1), U256::from(2), "two flattened nodes");
assert_eq!(word(&encoded, 1), U256::from(4), "four flattened nodes");

// Heads region begins right after the length word (word index 2), and the
// Heads region begins right after the array length word, and the
// per-element offsets there are relative to it.
let heads_base = 2 * 32;
let root_idx = (heads_base + word(&encoded, 2).as_usize()) / 32;
let child_idx = (heads_base + word(&encoded, 3).as_usize()) / 32;
let element_idx = |index: usize| (heads_base + word(&encoded, 2 + index).as_usize()) / 32;
let root_idx = element_idx(0);
let first_child_idx = element_idx(1);
let second_child_idx = element_idx(2);
let grandchild_idx = element_idx(3);

// Root: messageType Internal (1), onAcceptance true, parent = sentinel.
assert_eq!(
Expand All@@ -138,21 +142,21 @@ fn nested_internal_flattens_with_parent_pointers() {
"root parent = sentinel"
);

// Child: messageType External (0), parent index = 0 (root is first flattened node).
// Both children precede the grandchild in BFS order.
assert_eq!(
word(&encoded, child_idx),
U256::from(0),
"child messageType External"
word(&encoded, first_child_idx + 2),
U256::zero(),
"first child parent index 0"
);
assert_eq!(
word(&encoded, child_idx + 1),
word(&encoded, second_child_idx + 2),
U256::zero(),
"child onAcceptance false"
"second child parent index 0"
);
assert_eq!(
word(&encoded, child_idx + 2),
U256::zero(),
"child parent index 0"
word(&encoded, grandchild_idx + 2),
U256::one(),
"grandchild parent index 1"
);
}

Expand All@@ -162,12 +166,13 @@ fn nested_internal_flattens_with_parent_pointers() {
fn internal_params_encode_derived_appeal_rounds() {
// appealRounds is not stored on the Rust side; it is reconstructed as
// len(rotations) - 1 when encoding.
let encoded = MessageAllocationNode::abi_encode(&[internal_node(
let root = internal_node(
genvm_modules_interfaces::On::Finalized,
10,
&[2, 3, 4],
vec![],
)]);
);
let encoded = root.abi_encode();

// Walk to the feeParams bytes inside the single element.
let heads_base = 2 * 32;
Expand Down
12 changes: 6 additions & 6 deletions executor/fuzz/genvm-storage.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,31 +140,31 @@ async fn run_storage_fuzz(input: FuzzInput) -> anyhow::Result<()> {
address,
genvm::rt::vm::storage::Limiter::new(sync::DArc::new(
rt::fees::DataLimit::new(
vec![primitive_types::U256::MAX],
std::collections::HashMap::from([("test".to_owned(), primitive_types::U256::MAX)]),
genvm::config::FeesConfig {
expr_prelude: String::new(),
storage: genvm::config::FeesBucketConfig {
bucket_no: vec![0],
buckets: vec![symbol_table::GlobalSymbol::from("test")],
subtract_on_start_expr: "0".into(),
delta_expr: r"\attrs = 0".into(),
},
message_receipt: genvm::config::FeesBucketConfig {
bucket_no: vec![0],
buckets: vec![symbol_table::GlobalSymbol::from("test")],
subtract_on_start_expr: "0".into(),
delta_expr: r"\attrs = 0".into(),
},
nondet_output: genvm::config::FeesBucketConfig {
bucket_no: vec![0],
buckets: vec![symbol_table::GlobalSymbol::from("test")],
subtract_on_start_expr: "0".into(),
delta_expr: r"\attrs = 0".into(),
},
message_fee: genvm::config::FeesBucketConfig {
bucket_no: vec![0],
buckets: vec![symbol_table::GlobalSymbol::from("test")],
subtract_on_start_expr: "0".into(),
delta_expr: r"\attrs = 0".into(),
},
event: genvm::config::FeesBucketConfig {
bucket_no: vec![0],
buckets: vec![symbol_table::GlobalSymbol::from("test")],
subtract_on_start_expr: "0".into(),
delta_expr: r"\attrs = 0".into(),
},
Expand Down
Loading