Skip to content

Commit 843cae0

Browse files
committed
chore: migrate from bincode to bitcode
Reason: the bincode crate is no longer maintained [1]. Notable changes made during the migration: - Firecracker versions using bitcode can no longer parse snapshots produced by Firecracker versions using bincode and vice versa. Note that Firecracker does not support snapshot compatibility across its versions anyway. - RSS overhead has increased and exceeded 5 MB limit in certain tests. The memory monitor's threshold has been updated to 6 MB. - Since, unlike bincode, bitcode does not support with_fixed_int_encoding mode, the snapshot CRC field is now written as plain bytes to guarantee that its length is not going to change due to potential future changes in the serialisation logic. - Added a new argument to the seccompiler-bin to produce per-thread filter files to facilitate testing. Previously, the test_validate_filter was parsing the bincode-encoded filters itself. With individual per-thread filter files this is no longer required. [1] https://rustsec.org/advisories/RUSTSEC-2025-0141 Signed-off-by: Nikita Kalyazin <kalyazin@amazon.com>
1 parent 9fb6fcc commit 843cae0

File tree

35 files changed

+392
-507
lines changed

35 files changed

+392
-507
lines changed

Cargo.lock

Lines changed: 38 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/seccompiler.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
Seccompiler-bin is a tool that compiles seccomp filters expressed as JSON files
66
into serialized, binary BPF code that is directly consumed by Firecracker, at
7-
build or launch time.
7+
build or launch time. The binary filters are serialized using bitcode format.
88

99
Seccompiler-bin uses a custom [JSON file structure](#json-file-format), detailed
1010
further below, that the filters must adhere to.
1111

1212
Besides the seccompiler-bin executable, seccompiler also exports a library
1313
interface, with helper functions for deserializing and installing the binary
14-
filters.
14+
filters. The library uses bitcode format for serialization and deserialization.
1515

1616
## Usage
1717

@@ -31,13 +31,30 @@ Example usage:
3131
# [default: "seccomp_binary_filter.out"]
3232
--basic # Optional, creates basic filters, discarding any parameter checks.
3333
# (Deprecated).
34+
--split-output # Optional, creates individual BPF files for each thread
35+
# in addition to the main bitcode output file (for testing).
3436
```
3537

3638
### Seccompiler library
3739

3840
To view the library documentation, navigate to the seccompiler source code, in
3941
`firecracker/src/seccompiler/src` and run `cargo doc --lib --open`.
4042

43+
### Output format
44+
45+
Seccompiler-bin generates binary BPF filters serialized using the bitcode
46+
format. The output file contains a bitcode-serialized map of thread names to
47+
their corresponding BPF instruction sequences.
48+
49+
When using the `--split-output` flag, seccompiler-bin will generate:
50+
51+
- The main bitcode output file (as specified by `--output-file`)
52+
- Individual `.bpf` files for each thread containing raw BPF bytecode (useful
53+
for testing)
54+
55+
The individual thread files are named `<thread_name>.bpf` and placed in the same
56+
directory as the main output file.
57+
4158
## Where is seccompiler implemented?
4259

4360
Seccompiler is implemented as another package in the Firecracker cargo

docs/snapshotting/versioning.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ limitations.
66

77
## Introduction
88

9-
Firecracker uses the serde crate [1] along with the bincode [2] format to
9+
Firecracker uses the serde crate [1] along with the bitcode [2] format to
1010
serialize its state into Firecracker snapshots. Firecracker snapshots have
1111
versions that are independent of Firecracker versions. Each Firecracker version
1212
declares support for a specific snapshot data format version. When creating a
@@ -46,22 +46,22 @@ A Firecracker snapshot has the following format:
4646
| -------- | ---- | --------------------------------------------------------- |
4747
| magic_id | 64 | Firecracker snapshot and architecture (x86_64/aarch64). |
4848
| version | M | The snapshot data format version (`MAJOR.MINOR.PATCH`) |
49-
| state | N | Bincode blob containing the microVM state. |
49+
| state | N | Bitcode blob containing the microVM state. |
5050
| crc | 64 | Optional CRC64 sum of magic_id, version and state fields. |
5151

5252
The snapshot format has its own version encoded in the snapshot file itself
5353
after the snapshot's `magic_id`. The snapshot format version is independent of
5454
the Firecracker version and it is of the form `MAJOR.MINOR.PATCH`.
5555

5656
Currently, Firecracker uses the
57-
[Serde bincode encoder](https://github.com/servo/bincode) for serializing the
58-
microVM state. The encoding format that bincode uses does not allow backwards
59-
compatible changes in the state, so essentially every change in the microVM
60-
state description will result in bump of the format's `MAJOR` version. If the
61-
needs arises, we will look into alternative formats that allow more flexibility
62-
with regards to backwards compatibility. If/when this happens, we will define
63-
how changes in the snapshot format reflect to changes in its `MAJOR.MINOR.PATCH`
64-
version.
57+
[Serde bitcode encoder](https://github.com/SoftbearStudios/bitcode) for
58+
serializing the microVM state. The encoding format that bitcode uses does not
59+
allow backwards compatible changes in the state, so essentially every change in
60+
the microVM state description will result in bump of the format's `MAJOR`
61+
version. If the needs arises, we will look into alternative formats that allow
62+
more flexibility with regards to backwards compatibility. If/when this happens,
63+
we will define how changes in the snapshot format reflect to changes in its
64+
`MAJOR.MINOR.PATCH` version.
6565

6666
## VM state encoding
6767

@@ -72,14 +72,14 @@ Rust support are hard requirements while all others can be the subject of trade
7272
offs. More info about this comparison can be found
7373
[here](https://github.com/firecracker-microvm/firecracker/blob/9d427b33d989c3225d874210f6c2849465941dc0/docs/snapshotting/design.md#snapshot-format).
7474

75-
Key benefits of using *bincode*:
75+
Key benefits of using *bitcode*:
7676

7777
- Minimal snapshot size overhead
7878
- Minimal CPU overhead
7979
- Simple implementation
8080

8181
The current implementation relies on the
82-
[Serde bincode encoder](https://github.com/servo/bincode).
82+
[Serde bitcode encoder](https://github.com/SoftbearStudios/bitcode).
8383

8484
## Snapshot compatibility
8585

@@ -146,4 +146,4 @@ repository. All Firecracker devices implement the
146146
interface that enables creating from and saving to the microVM state.
147147

148148
[1]: https://serde.rs
149-
[2]: https://github.com/bincode-org/bincode
149+
[2]: https://github.com/SoftbearStudios/bitcode

src/firecracker/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@ fn main() {
5151
println!("cargo:rerun-if-changed={}", SECCOMPILER_SRC_DIR);
5252

5353
let out_path = format!("{}/{}", out_dir, ADVANCED_BINARY_FILTER_FILE_NAME);
54-
seccompiler::compile_bpf(&seccomp_json_path, &target_arch, &out_path, false)
54+
seccompiler::compile_bpf(&seccomp_json_path, &target_arch, &out_path, false, false)
5555
.expect("Cannot compile seccomp filters");
5656
}

src/seccompiler/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ path = "src/bin.rs"
1616
bench = false
1717

1818
[dependencies]
19-
bincode = { version = "2.0.1", features = ["serde"] }
19+
bitcode = { version = "0.6.9", features = ["serde"] }
2020
clap = { version = "4.5.53", features = ["derive", "string"] }
2121
displaydoc = "0.2.5"
2222
libc = "0.2.178"

src/seccompiler/src/bin.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ struct Cli {
2727
and rule-level actions. Not recommended."
2828
)]
2929
basic: bool,
30+
#[arg(
31+
long,
32+
help = "Output individual BPF files for each thread instead of a single combined file. \
33+
Used for testing purposes."
34+
)]
35+
split_output: bool,
3036
}
3137

3238
fn main() -> Result<(), CompilationError> {
@@ -36,5 +42,6 @@ fn main() -> Result<(), CompilationError> {
3642
&cli.target_arch,
3743
&cli.output_file,
3844
cli.basic,
45+
cli.split_output,
3946
)
4047
}

src/seccompiler/src/lib.rs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,19 @@ use std::os::fd::{AsRawFd, FromRawFd};
88
use std::os::unix::fs::MetadataExt;
99
use std::str::FromStr;
1010

11-
use bincode::config;
12-
use bincode::config::{Configuration, Fixint, Limit, LittleEndian};
13-
use bincode::error::EncodeError as BincodeError;
14-
1511
mod bindings;
1612
use bindings::*;
1713

1814
pub mod types;
1915
pub use types::*;
2016
use zerocopy::IntoBytes;
2117

22-
// This byte limit is passed to `bincode` to guard against a potential memory
18+
// This byte limit is passed to `bitcode` to guard against a potential memory
2319
// allocation DOS caused by binary filters that are too large.
2420
// This limit can be safely determined since the maximum length of a BPF
2521
// filter is 4096 instructions and Firecracker has a finite number of threads.
2622
const DESERIALIZATION_BYTES_LIMIT: usize = 100_000;
2723

28-
pub const BINCODE_CONFIG: Configuration<LittleEndian, Fixint, Limit<DESERIALIZATION_BYTES_LIMIT>> =
29-
config::standard()
30-
.with_fixed_int_encoding()
31-
.with_limit::<DESERIALIZATION_BYTES_LIMIT>()
32-
.with_little_endian();
33-
3424
/// Binary filter compilation errors.
3525
#[derive(Debug, thiserror::Error, displaydoc::Display)]
3626
pub enum CompilationError {
@@ -61,14 +51,15 @@ pub enum CompilationError {
6151
/// Cannot create output file: {0}
6252
OutputCreate(std::io::Error),
6353
/// Cannot serialize bfp: {0}
64-
BincodeSerialize(BincodeError),
54+
BitcodeSerialize(bitcode::Error),
6555
}
6656

6757
pub fn compile_bpf(
6858
input_path: &str,
6959
arch: &str,
7060
out_path: &str,
7161
basic: bool,
62+
split_output: bool,
7263
) -> Result<(), CompilationError> {
7364
let mut file_content = String::new();
7465
File::open(input_path)
@@ -188,9 +179,44 @@ pub fn compile_bpf(
188179
bpf_map.insert(name.clone(), bpf);
189180
}
190181

191-
let mut output_file = File::create(out_path).map_err(CompilationError::OutputCreate)?;
182+
// Helper function to create and write the main bitcode output file
183+
let write_main_output = || -> Result<(), CompilationError> {
184+
let mut output_file = File::create(out_path).map_err(CompilationError::OutputCreate)?;
185+
let encoded = bitcode::serialize(&bpf_map).map_err(CompilationError::BitcodeSerialize)?;
186+
187+
// Check size limit to prevent DOS attacks
188+
if encoded.len() > DESERIALIZATION_BYTES_LIMIT {
189+
// Create a simple error by trying to deserialize invalid data
190+
let error = bitcode::deserialize::<()>(&[0xFF]).unwrap_err();
191+
return Err(CompilationError::BitcodeSerialize(error));
192+
}
192193

193-
bincode::encode_into_std_write(&bpf_map, &mut output_file, BINCODE_CONFIG)
194-
.map_err(CompilationError::BincodeSerialize)?;
194+
std::io::Write::write_all(&mut output_file, &encoded)
195+
.map_err(CompilationError::OutputCreate)
196+
};
197+
198+
if split_output {
199+
// Output individual files for each thread (for testing)
200+
use std::path::Path;
201+
let base_path = Path::new(out_path);
202+
let parent = base_path.parent().unwrap_or_else(|| Path::new("."));
203+
204+
for (thread_name, bpf_data) in &bpf_map {
205+
let thread_file_path = parent.join(format!("{}.bpf", thread_name));
206+
let mut thread_file =
207+
File::create(&thread_file_path).map_err(CompilationError::OutputCreate)?;
208+
209+
// Write raw BPF data as bytes
210+
use zerocopy::IntoBytes;
211+
std::io::Write::write_all(&mut thread_file, bpf_data.as_bytes())
212+
.map_err(CompilationError::OutputCreate)?;
213+
}
214+
215+
// Also create the main output file with bitcode format for compatibility
216+
write_main_output()?;
217+
} else {
218+
// Normal output: single bitcode file
219+
write_main_output()?;
220+
}
195221
Ok(())
196222
}

src/vmm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ acpi_tables = { path = "../acpi-tables" }
1919
arrayvec = { version = "0.7.6", optional = true }
2020
aws-lc-rs = "1.15.1"
2121
base64 = "0.22.1"
22-
bincode = { version = "2.0.1", features = ["serde"] }
22+
bitcode = { version = "0.6.9", features = ["serde"] }
2323
bitflags = "2.10.0"
2424
bitvec = { version = "1.0.1", features = ["atomic", "serde"] }
2525
byteorder = "1.5.0"

0 commit comments

Comments
 (0)