{"record":{"id":"17a26ddf44cd3388","repo":"linera-io/linera-protocol","slug":"log-index-out-of-range","errorCode":null,"errorMessage":"log_index out of range","messagePattern":"log_index out of range","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"linera-bridge/contracts/evm-bridge/src/contract.rs","lineNumber":235,"sourceCode":"        }\n\n        // 2. Verify receipt inclusion via MPT proof\n        let proof_bytes: Vec<Bytes> = proof_nodes\n            .iter()\n            .map(|n| Bytes::copy_from_slice(n))\n            .collect();\n        proof::verify_receipt_inclusion(receipts_root, tx_index, receipt_rlp, &proof_bytes)\n            .expect(\"receipt inclusion proof failed\");\n\n        // 3. Decode receipt logs and parse the deposit event\n        let logs = proof::decode_receipt_logs(receipt_rlp).expect(\"failed to decode receipt logs\");\n        // `log_index` is a u64 but indexes a Vec (usize). On wasm32 `usize` is\n        // 32-bit, so an unchecked `as usize` cast would truncate — letting\n        // `log_index` and `log_index + 2^32` select the same log while hashing\n        // to different `DepositKey`s (replay-guard bypass → double mint). A\n        // checked cast rejects any value that does not fit `usize`; the full\n        // u64 is preserved for the `DepositKey` below.\n        let log_index_usize = usize::try_from(log_index).expect(\"log_index out of range\");\n        assert!(\n            log_index_usize < logs.len(),\n            \"log_index {} out of range (receipt has {} logs)\",\n            log_index,\n            logs.len()\n        );\n        let bridge_contract_bytes =\n            self.state.bridge_contract_address.get().expect(\n                \"bridge contract address not registered — call RegisterFungibleBridge first\",\n            );\n        let bridge_contract = alloy_primitives::Address::from(bridge_contract_bytes);\n        let deposit = proof::parse_deposit_event(&logs[log_index_usize], bridge_contract)\n            .expect(\"failed to parse DepositInitiated event\");\n\n        // 4. Validate deposit fields against bridge parameters\n        assert_eq!(\n            deposit.source_chain_id.as_limbs()[0],\n            params.source_chain_id,","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-bridge/contracts/evm-bridge/src/contract.rs#L217-L253","documentation":"process_deposit converts the u64 log_index to usize with usize::try_from and expects success. On wasm32 (where Linera contracts run), usize is 32-bit, so any log_index >= 2^32 fails here; the following assert! then separately rejects indices >= logs.len(). The checked cast is a documented replay-guard: an unchecked cast would alias log_index and log_index + 2^32 to the same log while hashing them to different DepositKeys, enabling a double mint.","triggerScenarios":"A relayer bug or a malicious submitter crafts log_index with the high 32 bits set (e.g. 0x1_0000_0001) targeting a real log; log_index is computed with an arithmetic overflow that produces a huge value; fuzzing the ProcessDeposit operation with extreme integers.","commonSituations":"Adversarial probing of the bridge by third parties; relayer integer bugs when log_index is derived from packed bitfields; test suites exercising the wasm32 truncation edge case described in the source comment.","solutions":["As the submitter: validate 0 <= log_index < receipt.logs.len() and log_index <= u32::MAX before submitting, since the contract runs on wasm32","Fix relayer arithmetic that packs or shifts log indices into high bits","If you maintain the contract and want a gentler failure, keep the checked cast but convert the expect into an assert! with the offending value in the message","Treat any occurrence in production as a red flag: inspect who submitted the operation, since values >= 2^32 indicate malice rather than accident"],"exampleFix":"// before (submitter skips validation)\nlet log_index = compute_log_index(); // may exceed u32::MAX on wasm32\nsubmit(ProcessDeposit { log_index, .. }); // aborts: log_index out of range\n\n// after (preflight in the relayer)\nlet log_index = compute_log_index();\nassert!(log_index <= u32::MAX as u64, \"log_index {log_index} not representable on wasm32\");\nassert!((log_index as usize) < receipt.logs.len(), \"log_index beyond receipt logs\");\nsubmit(ProcessDeposit { log_index, .. });","handlingStrategy":"validation","validationCode":"// Preflight both guards before submitting:\nassert!(log_index <= u32::MAX as u64, \"wasm32 usize overflow\");\nassert!((log_index as usize) < logs.len(), \"index beyond receipt logs\");\nsubmit(ProcessDeposit { log_index, .. });","typeGuard":"fn log_index_submittable(log_index: u64, logs_len: usize) -> bool {\n    log_index <= u32::MAX as u64 && (log_index as usize) < logs_len\n}","tryCatchPattern":"// A failing ProcessDeposit aborts atomically; alert on it — indices >= 2^32\n// indicate a relayer integer bug or deliberate probing, both worth paging on.","preventionTips":["Never compute log_index via bit shifting or packed fields","Fuzz relayer index arithmetic around u32 boundaries","Remember the contract executes on wasm32 with 32-bit usize even if your relayer is 64-bit"],"tags":["linera","bridge","wasm32","usize","integer-overflow","replay-guard","panic"],"backgroundTag":"index-out-of-range","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}