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
10 changes: 7 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff 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-manager/security/advisories/new),
or email kira@genlayerlabs.com
**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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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 Down
12 changes: 6 additions & 6 deletions executor/fuzz/genvm-storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,31 +151,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
12 changes: 6 additions & 6 deletions executor/install/config/genvm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,35 @@ modules:

log_level: info

# NOTE: we on purpose set all to 0 in this legacy version
# This legacy executor line intentionally charges every fee category at zero
fees:
expr_prelude: ""
storage:
bucket_no: 0
buckets: execution_data_gas
subtract_on_start_expr: |
0
delta_expr: |
\a = 0
message_receipt:
bucket_no: 0
buckets: execution_data_gas
subtract_on_start_expr: |
0
delta_expr: |
\a = 0
nondet_output:
bucket_no: 0
buckets: execution_data_gas
subtract_on_start_expr: |
0
delta_expr: |
\a = 0
message_fee:
bucket_no: 0
buckets: execution_data_gas
subtract_on_start_expr: |
0
delta_expr: |
\a = 0
event:
bucket_no: 0
buckets: execution_data_gas
subtract_on_start_expr: |
0
delta_expr: |
Expand Down
37 changes: 20 additions & 17 deletions executor/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,34 @@ fn default_fee_expr_zero() -> String {
"0".to_owned()
}

fn deserialize_bucket_nos<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
fn deserialize_bucket_names<'de, D>(d: D) -> Result<Vec<symbol_table::GlobalSymbol>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;

struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Vec<u8>;
type Value = Vec<symbol_table::GlobalSymbol>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an integer or array of integers")
f.write_str("a non-empty string or array of non-empty strings")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Vec<u8>, E> {
u8::try_from(v)
.map(|b| vec![b])
.map_err(|_| E::custom(format!("bucket_no {v} exceeds u8 range")))
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Vec<u8>, E> {
u8::try_from(v)
.map(|b| vec![b])
.map_err(|_| E::custom(format!("bucket_no {v} out of u8 range")))
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v.is_empty() {
return Err(E::custom("bucket name must not be empty"));
}
Ok(vec![symbol_table::GlobalSymbol::from(v)])
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<u8>, A::Error> {
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut v = Vec::new();
while let Some(n) = seq.next_element::<u8>()? {
v.push(n);
while let Some(name) = seq.next_element::<String>()? {
if name.is_empty() {
return Err(de::Error::custom("bucket name must not be empty"));
}
v.push(symbol_table::GlobalSymbol::from(name));
}
if v.is_empty() {
return Err(de::Error::custom("buckets must have at least one entry"));
}
Ok(v)
}
Expand All @@ -49,9 +51,10 @@ where
}

#[derive(Clone, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct FeesBucketConfig {
#[serde(deserialize_with = "deserialize_bucket_nos")]
pub bucket_no: Vec<u8>,
#[serde(deserialize_with = "deserialize_bucket_names")]
pub buckets: Vec<symbol_table::GlobalSymbol>,
/// Cost charged once, up-front, when the bucket is created
/// (the fixed part of `start + sum of per-change`).
#[serde(default = "default_fee_expr_zero")]
Expand Down
17 changes: 10 additions & 7 deletions executor/src/domain/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ use primitive_types::U256;

mod abi;

pub const CALL_KEY_WILDCARD: genlayer_sdk::abi::CallKey = genlayer_sdk::abi::CallKey([
0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0,
0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70,
]);

#[derive(
Debug,
Clone,
Expand Down Expand Up @@ -82,7 +87,7 @@ pub struct MessageAllocationNode {
/// Target contract address; `None` means wildcard (any recipient).
pub recipient: Option<genlayer_sdk::calldata::Address>,
/// `None` = wildcard: all call keys for this recipient
/// (chain sentinel: `CALL_KEY_WILDCARD` = `bytes32(0)`).
/// (chain sentinel: `CALL_KEY_WILDCARD` = `keccak256("")`).
pub call_key: Option<genlayer_sdk::abi::CallKey>,
/// Max budget for matching messages.
pub budget: U256,
Expand All @@ -93,12 +98,10 @@ pub struct MessageAllocationNode {
}

impl MessageAllocationNode {
/// ABI-encodes the nested allocation tree as the chain's flat
/// `MessageAllocationNode[]` representation (matching `abi.encode(nodes)`),
/// flattening children to parent-pointer form via pre-order traversal so that
/// every parent precedes its children and their `parentIndex` is well-defined.
pub fn abi_encode(roots: &[MessageAllocationNode]) -> Vec<u8> {
abi::encode(roots)
/// ABI-encodes this matched node and its descendants for transport to the chain.
/// The matched node is element 0 and descendants are in BFS order.
pub fn abi_encode(&self) -> Vec<u8> {
abi::encode(self)
}

#[allow(clippy::if_same_then_else)]
Expand Down
32 changes: 10 additions & 22 deletions executor/src/domain/fees/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,46 +50,34 @@ fn push_address(buf: &mut Vec<u8>, addr: Option<&genlayer_sdk::calldata::Address
}
}

/// `callKey` is already a 32-byte word; `None` is the `CALL_KEY_WILDCARD` = `bytes32(0)`.
/// `callKey` is already a 32-byte word; `None` is `CALL_KEY_WILDCARD`.
fn push_call_key(buf: &mut Vec<u8>, call_key: Option<&genlayer_sdk::abi::CallKey>) {
match call_key {
Some(ck) => buf.extend_from_slice(&ck.0),
None => buf.extend_from_slice(&[0u8; 32]),
None => buf.extend_from_slice(&super::CALL_KEY_WILDCARD.0),
}
}

/// ABI-encodes the nested allocation tree as the chain's flat
/// `MessageAllocationNode[]` representation (matching `abi.encode(nodes)`).
pub(super) fn encode(roots: &[MessageAllocationNode]) -> Vec<u8> {
// Pre-order flatten: (node, parentIndex). Parents always precede children,
// so the parent's array index is already assigned when a child is visited.
/// Encodes the transport payload consumed by `decodeAllocationSubtree`.
pub(super) fn encode(root: &MessageAllocationNode) -> Vec<u8> {
let mut flat: Vec<(&MessageAllocationNode, U256)> = Vec::new();
fn flatten<'a>(
nodes: &'a [MessageAllocationNode],
parent: U256,
out: &mut Vec<(&'a MessageAllocationNode, U256)>,
) {
for node in nodes {
let my_index = U256::from(out.len() as u64);
out.push((node, parent));
flatten(&node.children, my_index, out);
let mut queue = std::collections::VecDeque::from([(root, NODE_ROOT_SENTINEL)]);
while let Some((node, parent_index)) = queue.pop_front() {
let node_index = U256::from(flat.len());
flat.push((node, parent_index));
for child in &node.children {
queue.push_back((child, node_index));
}
}
flatten(roots, NODE_ROOT_SENTINEL, &mut flat);

// Encode each element as its own self-contained dynamic tuple.
let elements: Vec<Vec<u8>> = flat
.iter()
.map(|(node, parent_index)| encode_node(node, *parent_index))
.collect();

// `abi.encode(MessageAllocationNode[])`: leading offset (0x20) to the array.
let mut buf = Vec::new();
push_word(&mut buf, 0x20);

// Dynamic array of dynamic elements: length, then per-element head offsets
// (relative to the start of the head region, i.e. right after the length),
// then the element tails.
push_word(&mut buf, elements.len() as u64);
let mut offset = elements.len() * 32;
for element in &elements {
Expand Down
53 changes: 30 additions & 23 deletions executor/src/exe/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ const EXECUTION_DATA_HELP: &str = "path to file containing encoded execution dat

fn fill_nested_fee_buckets(
is_nested: bool,
max_bucket_no: usize,
bucket_totals: &mut Vec<primitive_types::U256>,
bucket_names: &[symbol_table::GlobalSymbol],
bucket_totals: &mut std::collections::HashMap<String, primitive_types::U256>,
) {
if is_nested && bucket_totals.is_empty() {
bucket_totals.resize(max_bucket_no + 1, primitive_types::U256::zero());
bucket_totals.extend(
bucket_names
.iter()
.copied()
.map(|name| (name.as_str().to_owned(), primitive_types::U256::zero())),
);
}
}

Expand Down Expand Up @@ -162,7 +167,8 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> {
let mut bucket_totals = execution_data
.bucket_totals
.iter()
.map(|bi| {
.map(|(name, bi)| {
anyhow::ensure!(!name.is_empty(), "bucket name must not be empty");
let (sign, bytes) = bi.to_bytes_be();
anyhow::ensure!(
sign != num_bigint::Sign::Minus,
Expand All @@ -172,36 +178,37 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> {
let mut buf = [0u8; 32];
let start = 32usize.saturating_sub(bytes.len());
buf[start..].copy_from_slice(&bytes);
Ok(primitive_types::U256::from_big_endian(&buf))
Ok((name.clone(), primitive_types::U256::from_big_endian(&buf)))
})
.collect::<Result<Vec<_>>>()?;

let max_bucket_no = [
&config.fees.storage.bucket_no,
&config.fees.message_receipt.bucket_no,
&config.fees.nondet_output.bucket_no,
&config.fees.message_fee.bucket_no,
&config.fees.event.bucket_no,
.collect::<Result<std::collections::HashMap<_, _>>>()?;

let bucket_names = [
&config.fees.storage.buckets,
&config.fees.message_receipt.buckets,
&config.fees.nondet_output.buckets,
&config.fees.message_fee.buckets,
&config.fees.event.buckets,
]
.into_iter()
.flat_map(|v| v.iter().copied())
.max()
.unwrap_or(0);
.collect::<Vec<_>>();

fill_nested_fee_buckets(is_nested, max_bucket_no as usize, &mut bucket_totals);
anyhow::ensure!(
(max_bucket_no as usize) < bucket_totals.len(),
"fees config references bucket {max_bucket_no} but only {} bucket(s) provided",
bucket_totals.len(),
);
fill_nested_fee_buckets(is_nested, &bucket_names, &mut bucket_totals);
for name in bucket_names {
anyhow::ensure!(
bucket_totals.contains_key(name.as_str()),
"fees config references missing bucket `{}`",
name.as_str(),
);
}

let emit_leader_public_data =
!args.sync && !is_nested && execution_data.leader_public_data.is_none();
let (leader_nondet_results, malformed_leader_public_data) =
match execution_data.leader_public_data.as_ref() {
None => (None, false),
Some(encoded) => match genvm::leader_public_data::LeaderPublicData::decode(encoded) {
Ok(data) => (Some(data.nondet_block_outputs), false),
Ok(data) => (Some(data.nd_outs), false),
Err(_) => (Some(Vec::new()), true),
},
};
Expand Down Expand Up @@ -287,7 +294,7 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> {
let data_fees_consumed = runtime.block_on(shared_data.data_fees_limit.consumed());
let leader_public_data = if emit_leader_public_data {
genvm::leader_public_data::LeaderPublicData {
nondet_block_outputs: Vec::new(),
nd_outs: Vec::new(),
}
.encode()
} else {
Expand Down
6 changes: 3 additions & 3 deletions executor/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ impl FullResult {
emissions: Vec::new(),
nondet_disagreement: None,
leader_public_data: bytes::Bytes::new(),
data_fees_remaining: Vec::new(),
data_fees_remaining: std::collections::BTreeMap::new(),
data_fees_consumed: genvm_modules_interfaces::BucketsConsumed::default(),
llm_consumed_gen_wei: primitive_types::U256::zero(),
},
Expand All @@ -201,15 +201,15 @@ impl FullResult {
rt_result: rt::vm::FullResult,
leader_public_data: bytes::Bytes,
nondet_disagreement: Option<u32>,
data_fees_remaining: Vec<primitive_types::U256>,
data_fees_remaining: std::collections::BTreeMap<String, primitive_types::U256>,
data_fees_consumed: rt::fees::BucketsConsumed,
llm_consumption: primitive_types::U256,
) -> Self {
struct Hashable<'a> {
backtrace: &'a Option<rt::errors::Backtrace>,
data: &'a calldata::unparsed::Maybe<calldata::Value>,
data_fees_consumed: &'a rt::fees::BucketsConsumed,
data_fees_remaining: &'a Vec<primitive_types::U256>,
data_fees_remaining: &'a std::collections::BTreeMap<String, primitive_types::U256>,
kind: &'a public_abi::ResultCode,
wasm_store_hashes: &'a rt::errors::WasmStoreHashes,
storage_changes: &'a Vec<rt::vm::storage::Delta>,
Expand Down
Loading