The Problem: Smart Contracts Have Big Memories
Every smart contract platform faces the same fundamental tension: programs need memory to run, but loading that memory is expensive. A Gear program can address up to 32,768 WebAssembly pages (each 64 KB), totaling 2 GB of linear memory. In practice most programs use far less, but a program that has been running for a while accumulates state spread across many pages — and even a modest allocation of a few hundred pages means megabytes of data.
Now imagine a blockchain node that processes hundreds of messages per block, each targeting a different program. If the node loaded every program's entire memory before execution, it would spend the majority of its time on I/O — reading megabytes of data from storage, most of which the program never actually touches during that particular execution.
Gear solves this with lazy pages — a subsystem that loads memory pages only when the program actually reads or writes them. The name is borrowed from operating systems, where the same idea is called "demand paging." The mechanism is conceptually simple but technically intricate, because it operates at the boundary between WebAssembly execution, operating system signals, and blockchain storage.
The Core Idea
The principle behind lazy pages is straightforward: make all of a program's memory inaccessible at first, then intercept the CPU fault that occurs when the program tries to use it.
Before a Gear program begins executing, the lazy-pages subsystem marks all of its memory pages (except for the auxiliary stack region — more on that later) as protected using the operating system's mprotect system call. This tells the OS: "if anything tries to read or write these addresses, don't allow it — send me a signal instead."
When the WebAssembly program runs and accesses a protected page, the CPU raises a fault. On Linux this arrives as a SIGSEGV signal; on macOS it may be SIGBUS. Gear's signal handler catches the fault, determines which page was accessed and whether the access was a read or a write, loads the page data from persistent storage if needed, removes the protection from that page, and returns control to the program. The program continues as if nothing happened — it never knows its memory was loaded on demand.
This is not a new idea in computing. Operating systems have done demand paging since the 1960s. What makes Gear's implementation interesting is that it adapts this OS-level technique to a very different context: metered WebAssembly execution on a blockchain, where every memory access has a gas cost and correctness is critical for consensus.
Anatomy of a Page Fault
When a SIGSEGV arrives, the signal handler receives detailed information about what happened. The OS provides the faulting address (which memory location the program tried to access) and, through platform-specific CPU registers, whether the access was a read or a write.
This read-vs-write distinction matters for two reasons. First, it affects gas costs — writes are more expensive than reads because they require persisting state changes. Second, it determines what protection level to apply after handling the fault.
The platform detection is surprisingly non-trivial. On Linux x86_64, the handler examines the REG_ERR register's second bit. On ARM64, it reads the ESR (Exception Syndrome Register) and checks the WNR (Write Not Read) bit. macOS uses different mechanisms for each architecture. The lazy-pages crate abstracts all of this behind a unified interface, but underneath it is carefully tuned platform-specific code.
Once the handler knows the faulting page and access type, it follows a state machine:
Unaccessed page, read access: The handler charges the program for a read operation (deducting gas), loads the page data from storage if the page has persisted content, sets the page to read-only (allowing future reads without another fault, but still trapping writes), and records the page as "accessed."
Unaccessed page, write access: More expensive. The handler charges for a write, loads existing data from storage (because the program might read-modify-write), fully unprotects the page, and records it as both "accessed" and "write-accessed."
Previously read page, now written: The handler charges only the incremental cost of upgrading from read to write, removes remaining protection, and adds the page to the write-accessed set.
Already write-accessed page: This should never generate a fault, because the page is already fully unprotected. If it happens, something has gone wrong — the system treats this as an error.
An important detail: a program pays gas only on the first access to a page (and once more if a read-only page is later written to). After that, the page is unprotected and all subsequent reads and writes to it are free — the signal handler is never invoked again for that page during the current execution. So "lazy" doesn't mean "expensive on every touch" — it means "pay once, use freely."
Tracking Changes: What Did the Program Touch?
After execution completes, the runtime needs to know which pages changed so it can persist them back to storage. Lazy pages tracks two sets: accessed pages (everything the program read or wrote) and write-accessed pages (only pages that were modified).
These sets use an interval-tree data structure that efficiently stores ranges of page numbers. If a program writes pages 10 through 50, the tree stores this as a single interval rather than 41 individual entries. This is important because the difference between accessed and write-accessed pages determines what needs to be written back to storage — and doing this efficiently matters for block processing time.
The write-accessed set is the critical output. After execution, only these pages need to be persisted. The accessed-but-not-written pages were loaded from storage, used in computations, but not modified — they can be discarded without loss.
Gas Metering Inside a Signal Handler
One of the most unusual aspects of Gear's lazy pages is that gas metering happens inside the signal handler. Every time a page fault occurs, the handler deducts gas from the program's remaining budget.
This works through a somewhat unusual mechanism. WebAssembly programs in Gear have a special global variable called gear_gas that tracks remaining gas. The signal handler accesses this global directly — reaching into the WASM instance's memory to read and update the gas counter. This requires platform-specific knowledge of how the WASM runtime (Wasmer or Wasmtime) lays out global variables in memory.
If the gas runs out during a page fault, the handler sets a special "gas limit exceeded" flag and removes all memory protections at once. This lets the program continue executing (it would be dangerous to abort mid-signal-handler), but the runtime checks the flag after the WASM function returns and treats the execution as failed.
The cost model distinguishes seven different charge categories: signal-triggered reads, signal-triggered writes, signal-triggered writes after a previous read, host-function-triggered reads, host-function-triggered writes, host-function-triggered writes after a previous read, and page data loading from storage. This granularity allows the protocol to price memory operations accurately — a write to a page that has stored data is more expensive than a write to a fresh page, because the old data must be loaded first.
Host Function Pre-processing
Not all memory accesses come through page faults. When a program calls a host function (a system call provided by the Gear runtime), the runtime knows in advance which memory regions the function will access. In these cases, lazy pages can process the affected pages proactively — loading data and updating protections before the host function runs, rather than waiting for individual faults.
This pre-processing path takes arrays of memory intervals (one for reads, one for writes) and handles all the affected pages in batch. This is more efficient than triggering individual page faults because it avoids the overhead of signal delivery and handler invocation for each page.
The distinction between signal-triggered and host-function-triggered accesses also appears in the cost model, allowing the protocol to price pre-processed accesses differently if desired.
Signal Handler Safety
Writing correct code inside a signal handler is notoriously difficult. Signal handlers run in a restricted context — many standard library functions are not safe to call, and the handler must be careful not to corrupt the state of the interrupted program.
Gear's signal handler addresses this in several ways. It runs on a dedicated alternate signal stack (configured with SA_ONSTACK), so it doesn't consume the program's stack space. It uses SA_NODEFER to allow nested signal handling in specific scenarios. And it chains to the previous signal handler (typically Wasmer's or Wasmtime's) for faults that don't correspond to lazy-pages-protected memory — this is important because the WASM runtime has its own signal-based mechanisms (for example, bounds checking).
The handler also uses thread-local storage for all its state, avoiding any need for synchronization. Each execution thread has its own context containing the page tracking sets, gas charger, and storage interface. This makes the system safe for concurrent execution of different programs on different threads.
The Auxiliary Stack
When Rust (or C/C++) compiles to WebAssembly, the compiler places an auxiliary stack inside the WASM linear memory. This is separate from the WebAssembly operand stack, which is managed by the VM itself and is not part of linear memory at all. The auxiliary stack is used for things like stack-allocated variables that need their address taken, local arrays, and data passed by reference across function calls — essentially anything that requires a pointer in the source language.
This auxiliary stack lives at the beginning of WASM linear memory and is accessed intensively during execution. Protecting these pages with lazy-pages would cause a flood of page faults on nearly every function call, which would be prohibitively expensive in terms of both gas and performance.
Gear recognizes this and treats the auxiliary stack specially. During initialization, lazy pages receives a stack_endparameter indicating where this region ends. All pages within that region are left unprotected from the start — the program can read and write them freely without any signal handler overhead. This also means those pages are not tracked for storage persistence, saving the user from paying for storing what is essentially scratch space that gets rebuilt on every execution.
Only memory above the auxiliary stack boundary — where the program's heap lives, containing long-lived allocated data — is subject to lazy loading and gas-metered access.
Why This Matters for Blockchain
The lazy-pages mechanism has several properties that make it particularly well-suited for blockchain execution.
Fairness through metering. Each new page a program touches costs gas, which prevents denial-of-service attacks where a program allocates maximum memory to slow down the network. But once a page has been accessed, the program can use it freely for the rest of that execution without additional charges — so the cost is proportional to the footprint, not the intensity of use.
Deterministic execution. Despite the asynchronous nature of signal handling, the result is fully deterministic. Given the same program state and input message, the same pages will be accessed in the same order, the same gas will be charged, and the same final state will be produced. This is essential for consensus — all validators must arrive at the same result.
Efficient state diffs. By tracking exactly which pages were modified, the system produces minimal state diffs. Only changed pages are written back to storage, and only those diffs need to be propagated across the network.
Proportional I/O. A program that processes a simple message touching 3 pages loads only those 3 pages from storage, regardless of how large its total allocated memory is. This keeps block processing time proportional to actual computation rather than allocated resources.
The Bigger Picture
Lazy pages is one of those subsystems that works best when invisible. Program developers don't interact with it directly — they write standard Rust code compiled to WebAssembly, and the memory management happens transparently beneath them. Node operators benefit from reduced I/O without any configuration.
The technique demonstrates how ideas from operating system design can be adapted to blockchain contexts. The core principle — don't load what you don't need — is as old as virtual memory itself. But applying it to metered, deterministic, consensus-critical execution required careful engineering at every level: from platform-specific register inspection to gas-aware signal handling to efficient change tracking.
In Gear's architecture, lazy pages sits at a critical junction between the WASM sandbox and the storage layer, making the actor-model message passing practical at scale. Without it, every message dispatch would require loading potentially megabytes of program state. With it, programs pay only for what they use — and as it turns out, being lazy is sometimes the smartest thing you can do.
Vara
Website | X | Discord | Telegram | Wiki | GitHub
Gear Protocol
Website | X | Discord | Telegram | GitHub | Gear IDEA | Whitepaper
