Skip to content

.rodata section has writable (W) flag causing MemWriteOnExecutablePage on CKB2023 (CKB-VM v2) #150

Description

@radiiplus

Environment

Component Version
CKB Node 0.205.0 (CKB2023 consensus active)
CKB-VM v2 (CKB2023 hardfork, confirmed via get_consensus RPC — RFC 0048/0049 at epoch 0x0)
ckb-std 1.1.0 (latest on crates.io)
Rust Toolchain nightly, riscv64imac-unknown-none-elf target
Linker rust-lld

Problem Description

When deploying a CKB contract built with ckb-std 1.1.0 and default_alloc!() as a type script on a CKB2023-enabled chain, the transaction fails during verification with:

TransactionFailedToVerify: Verification failed Script(TransactionScriptError {
  source: Outputs[0].Type,
  cause: VM Internal Error: MemWriteOnExecutablePage
})

This occurs even though:

  1. CKB2023 is confirmed active (hardfork features at epoch 0)
  2. The ELF binary has properly page-aligned program headers (4KB boundaries)
  3. Code sections are mapped to separate pages from data sections

Root Cause

The .rodata section header in compiled contracts has the W (writable) flag set, which violates W^X (Write XOR Execute) memory protection enforced by CKB-VM.

ELF Analysis — Failing Contract (with ckb-std + default_alloc!())

Section Headers:
  [ 1] .text             PROGBITS  0x0000000000200000  004252  00  AX   4096   ← code: A+X ✓
  [ 2] .rodata           PROGBITS  0x0000000000205000  000168  00  WAM  4096   ← PROBLEM: W flag!
  [ 4] .data             PROGBITS  0x0000000000208000  0000a8  00  WA   4096   ← data: W+A ✓
  [ 5] .bss              NOBITS    0x0000000000209000  082000  00  WA   4096   ← heap:  W+A ✓
Program Headers:
  LOAD  0x001000  0x200000  0x004252  0x004252  R E   ← .text
  LOAD  0x006000  0x205000  0x000168  0x000168  R     ← .rodata (marked R, but section header has W)
  LOAD  0x009000  0x208000  0x0000a8  0x0000a8  RW    ← .data
  LOAD  0x00a000  0x209000  0x000000  0x082000  RW    ← .bss (532 KB heap)

The section header flags come from the compiled input objects. CKB-VM appears to validate section header flags (not just program header flags) for W^X compliance. The .rodata section with WAM flags (Write + Alloc + Merge) violates this check.

ELF Analysis — Minimal Contract (no ckb-std)

A bare #![no_std] #![no_main] contract with just main() -> i8 { 0 } produces a clean binary and executes successfully:

Section Headers:
  [ 1] .text  PROGBITS  0x0000000000200000  000004  00  AX  4096  ← only code, no W flag

Reproduction Steps

1. Create a minimal contract using ckb-std

# Cargo.toml
[package]
name = "test-contract"
version = "0.1.0"
edition = "2021"

[dependencies]
ckb-std = "1.1.0"

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
// src/main.rs
#![no_std]
#![no_main]

use ckb_std::default_alloc;

extern crate alloc;
default_alloc!();

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
pub fn main() -> i8 {
    0  // Always success
}

2. Build

cargo build --release --target riscv64imac-unknown-none-elf

3. Verify the binary

readelf -SW target/riscv64imac-unknown-none-elf/release/test-contract | grep rodata
# Output shows: .rodata PROGBITS ... WAM  ← W flag present

4. Deploy on CKB2023 chain

  1. Deploy the binary as a cell
  2. Create a transaction that uses this cell as a type script (not just a dep)
  3. Submit the transaction

Result: MemWriteOnExecutablePage verification failure

Expected Behavior

On CKB2023 (CKB-VM v2), the contract should execute successfully. The CKB-VM v2 architecture maps code and data to separate memory pages, eliminating W^X conflicts for properly structured binaries.

Actual Behavior

Transaction verification fails with MemWriteOnExecutablePage because CKB-VM detects a writable flag on a section that shares a page with executable code, or interprets the .rodata W flag as a W^X violation.

Investigation Findings

The .rodata W flag comes from ckb-std internals

The default_alloc!() macro and ckb-std's buddy allocator place metadata in sections that the Rust compiler marks as writable. This happens during compilation of ckb-std's own objects, which are then linked into the final binary.

Minimal contract (no ckb-std) works

A contract with zero dependencies and no allocator compiles cleanly with only a .text section (AX flags, no W) and executes without issue.

This affects all real contracts

Every contract in our protocol (vault, factory, pool, dex, launchpad, registry) uses ckb-std + default_alloc!() and exhibits the same .rodata WAM flags. The vault is the only one currently tested as an executing type script — but any of them would fail if used as a type script on CKB2023.

Impact

This issue blocks the use of ckb-std-based contracts as executing type scripts on any CKB2023 chain (devnet, testnet, mainnet). Without type script execution, contracts cannot enforce protocol rules on-chain — they can only serve as passive data cells with off-chain validation.

Possible Fixes

  1. In ckb-std: Ensure compiled objects do not place writable data in .rodata. The buddy allocator metadata and other static data should be placed in .data or .bss, not .rodata.

  2. In the toolchain: A linker script could force .rodata to be read-only, but this would require stripping the W flag from input sections, which may not be supported by rust-lld.

  3. In CKB-VM: If CKB-VM v2 only checks program headers (not section headers), the binary layout may already be compliant — but if it checks section headers, the compiled objects themselves need to be fixed.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions