Lighthouse Security Considerations
Rust is a language that allows developers to write safe and production-ready code, by automatically preventing multiple types of vulnerabilities related to unsafe memory management (amongst other types of nasty bugs). Particularly, safety bugs like buffer overflows, use-after-frees, segmentation faults and other vulnerabilities arising from poor memory access management are inherently mitigated by the Rust compiler. Specifically, Rust:
- handles memory management with the introduction of the Ownership concept;
- moves many run-time errors to compile-time errors;
- allows efficient and safe concurrency handling;
- uses Cargo for secure dependency management.
One of the key principles of Lighthouse is: never panic! Every single error/exception should be caught and handled in a safe way, following the Rust language philosophy. In case of an error, the calling code should attempt to recover in a way that is adequate for each situation.
At Sigma Prime, we understand the importance and the challenges related to writing bug-free production software. We are commited to building a fast and efficient client, that does not compromise security. To help us achieve this goal, we are setting up a thorough on-going security process as part of our development life cycle:
- All Pull Requests are to be manually reviewed by at least 1 "Core Developer" before merge;
- All finalised crates are to go through a series of extensive fuzzing.
Fuzzing is a process that allows the identification of bugs (not just security-related bugs) by providing randomised and unexpected data inputs to software with the purpose of causing crashes (or in Rust, panics) and other unexpected behaviours (e.g. memory leaks).
An effective fuzzer would generate payloads based on valid inputs to cover for as many code paths as possible, with the goal of exposing as many edge cases / uncovered scenarios as possible.
Making sure that code coverage is efficient can often be challenging, as this requires (amongst other considerations) binary instrumentation. This is why we decided to go for a modularised/crate-based approach: our fuzzing process will target all finalised modules individually. Our goal is to fuzz-test every Lighthouse component, where fuzzing is applicable.
Introducing our Fuzzing Toolset
libFuzzer is a library part of the LLVM project, enabling coverage-guided fuzz testing. This library focuses on:
- In-process fuzzing: the fuzzing engine executes the target many times with multiple data inputs in the same process. It must tolerate any kind of input (empty, huge, malformed, etc);
- White-box fuzzing: libFuzzer leverages compiler instrumentation and requires access to the source code
- Coverage-guided fuzzing: for every input/test case, libFuzzer tracks code paths (sections of the code reached), and produces variants of each test case to generate additional input data to increase code coverage.
The cargo-fuzz crate is a command-line wrapper for libFuzzer that allows easy and quick definition of fuzzing targets (i.e. specific entry point to send fuzzing input to).
American Fuzzy Lop (AFL)
AFL (or American Fuzzy Lop), is an open source fuzzer considered by many security researchers to be the most efficient and most advanced fuzzer available. Just like libFuzzer, AFL leverages compile-time instrumentation and combines it with a genetic algorithm, allowing it to expose and fuzz large sections of the assessed code, increasing code coverage of the testing payloads.
AFL's capabilities are quite impressive: in this example, it was used to generate valid bash scripts (which enabled the identification of multiple security-related bugs). Here, it was used to create valid images out of thin air.
AFL was used to find vulnerabilities in the following software:
- Kernels: Linux, OpenBSD, iOS, Android, etc.
- Web servers/browsers: Apache httpd, nginx, Mozilla Firefox, Apple Safari, Internet Explorer, etc.
A list of interesting security-related bugs attributable to AFL can be found here.
Previous Rust Fuzzing Work
While Rust is memory-safe by default (see above), it also allows developers to use the "Unsafe" keyword/feature to apply less restrictions than normal (e.g. allowing access and modification to a mutable static variable). Refer to the Rust book for more information on Unsafe Rust.
Fuzzing Rust applications has yielded a few interesting bugs/vulnerabilities. See this trophy case.
We are currently targeting the block validation module/crate with our fuzzing toolset. Once we're happy with our fuzzing targets and their related coverage, we will start integrating our fuzzing infrastructure with our CI processes, essentially triggering libFuzzer/AFL for each release of the crate.
For new crates/modules, a manual "on-boarding" process is necessary to tailor the fuzzing to the functions consuming these inputs. Once the fuzzing targets are finalised (and providing the crates/modules interfaces remain the same), these targets should be re-usable for all subsequent versions of the libraries/crates).
Our team is thinking about building a similar infrastructure for all other ETH 2.0 clients. If you think it's a good idea and if you'd like to help, please reach out!