Skip to content

Commit f83a04f

Browse files
authored
Fix guest call to halt not dropping allocated trace data leading to memory leak (#1072)
* [trace] Update flatbuffers schema to define an enum guest event to send to the host Signed-off-by: Doru Blânzeanu <[email protected]> * [trace] Rename KeyValue to EventKeyValue Signed-off-by: Doru Blânzeanu <[email protected]> * [trace] Add Encoder and Decoder traits Signed-off-by: Doru Blânzeanu <[email protected]> * [trace] Add EventsEncoder and EventsDecoder structs - This types help exchange GuestEvent types from guest to host - The encoder keeps a Flatbuffer Builder and takes a maximum capacity which when is close to be filled, it calls a function configured at initialization. - This is a mechanism to report to the host the serialized events. This mechanism serializes the events as they are created so it avoids reallocation of the Flatbuffer Builder buffer Signed-off-by: Doru Blânzeanu <[email protected]> * [trace] Update unit tests for the new Encoder/Decoder Signed-off-by: Doru Blânzeanu <[email protected]> * [trace-guest] Update tracing logic to use the encoder Signed-off-by: Doru Blânzeanu <[email protected]> * [trace] Update trace context to use the new decoder Signed-off-by: Doru Blânzeanu <[email protected]> * Add guest trace fuzzying - The fuzzying test runs a guest function that runs a recursive function that creates spans and events. - The rationale is to stress the logic that creates, serializes, de-serializes and emits the trace data to find errors that can crash the host or guest Signed-off-by: Doru Blânzeanu <[email protected]> * [trace-guest] Add event estimation for buffer preallocation to avoid reallocation by Flatbuffers Signed-off-by: Doru Blânzeanu <[email protected]> * Enable miri tests for hyperlight-common Signed-off-by: Doru Blânzeanu <[email protected]> --------- Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent 1cdc4bb commit f83a04f

File tree

23 files changed

+2123
-733
lines changed

23 files changed

+2123
-733
lines changed

.github/workflows/Fuzzing.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
fuzzing:
1414
uses: ./.github/workflows/dep_fuzzing.yml
1515
with:
16-
targets: '["fuzz_host_print", "fuzz_guest_call", "fuzz_host_call"]' # Pass as a JSON array
16+
targets: '["fuzz_host_print", "fuzz_guest_call", "fuzz_host_call", "fuzz_guest_estimate_trace_event", "fuzz_guest_trace"]' # Pass as a JSON array
1717
max_total_time: 18000 # 5 hours in seconds
1818
secrets: inherit
1919

@@ -26,7 +26,7 @@ jobs:
2626
steps:
2727
- name: Checkout code
2828
uses: actions/checkout@v6
29-
29+
3030
- name: Notify Fuzzing Failure
3131
run: ./dev/notify-ci-failure.sh --labels="area/fuzzing,area/testing,lifecycle/needs-review,release-blocker"
3232
env:

.github/workflows/ValidatePullRequest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
- docs-pr
6464
uses: ./.github/workflows/dep_fuzzing.yml
6565
with:
66-
targets: '["fuzz_host_print", "fuzz_guest_call", "fuzz_host_call"]' # Pass as a JSON array
66+
targets: '["fuzz_host_print", "fuzz_guest_call", "fuzz_host_call", "fuzz_guest_estimate_trace_event", "fuzz_guest_trace"]' # Pass as a JSON array
6767
max_total_time: 300 # 5 minutes in seconds
6868
docs_only: ${{needs.docs-pr.outputs.docs-only}}
6969
secrets: inherit

.github/workflows/dep_rust.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ jobs:
138138
env:
139139
TARGET_TRIPLE: ${{ inputs.target_triple }}
140140

141+
- name: Run Miri tests
142+
if: ${{ (runner.os == 'Linux' )}}
143+
env:
144+
CARGO_TERM_COLOR: always
145+
TARGET_TRIPLE: ${{ inputs.target_triple }}
146+
run: just miri-tests
147+
141148
- name: Run Rust tests
142149
env:
143150
CARGO_TERM_COLOR: always

Justfile

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ test-doc target=default-target features="":
214214
### LINTING ####
215215
################
216216

217+
miri-tests:
218+
rustup component add miri --toolchain nightly
219+
# For now only run miri tests on hyperlight-common with trace_guest feature
220+
# We can add more as needed
221+
cargo +nightly miri test -p hyperlight-common -F trace_guest
222+
217223
check:
218224
{{ cargo-cmd }} check {{ target-triple-flag }}
219225
{{ cargo-cmd }} check -p hyperlight-host --features crashdump {{ target-triple-flag }}
@@ -342,12 +348,14 @@ bench features="":
342348
fuzz_memory_limit := "4096"
343349

344350
# Fuzzes the given target
351+
# Uses *case* for compatibility to determine if the target is a tracing fuzzer or not
345352
fuzz fuzz-target:
346-
cargo +nightly fuzz run {{ fuzz-target }} --release -- -rss_limit_mb={{ fuzz_memory_limit }}
353+
case "{{ fuzz-target }}" in *trace*) just fuzz-trace {{ fuzz-target }} ;; *) cargo +nightly fuzz run {{ fuzz-target }} --release -- -rss_limit_mb={{ fuzz_memory_limit }} ;; esac
347354

348355
# Fuzzes the given target. Stops after `max_time` seconds
356+
# Uses *case* for compatibility to determine if the target is a tracing fuzzer or not
349357
fuzz-timed fuzz-target max_time:
350-
cargo +nightly fuzz run {{ fuzz-target }} --release -- -rss_limit_mb={{ fuzz_memory_limit }} -max_total_time={{ max_time }}
358+
case "{{ fuzz-target }}" in *trace*) just fuzz-trace-timed {{ max_time }} {{ fuzz-target }} ;; *) cargo +nightly fuzz run {{ fuzz-target }} --release -- -rss_limit_mb={{ fuzz_memory_limit }} -max_total_time={{ max_time }} ;; esac
351359

352360
# Builds fuzzers for submission to external fuzzing services
353361
build-fuzzers: (build-fuzzer "fuzz_guest_call") (build-fuzzer "fuzz_host_call") (build-fuzzer "fuzz_host_print")
@@ -356,6 +364,28 @@ build-fuzzers: (build-fuzzer "fuzz_guest_call") (build-fuzzer "fuzz_host_call")
356364
build-fuzzer fuzz-target:
357365
cargo +nightly fuzz build {{ fuzz-target }}
358366

367+
# Fuzzes the guest with tracing enabled
368+
fuzz-trace fuzz-target="fuzz_guest_trace":
369+
# We need to build the trace guest with the trace feature enabled
370+
just build-rust-guests release trace_guest
371+
just move-rust-guests release
372+
RUST_LOG="trace,hyperlight_guest=trace,hyperlight_guest_bin=trace" cargo +nightly fuzz run {{ fuzz-target }} --features trace --release -- -rss_limit_mb={{ fuzz_memory_limit }}
373+
# Rebuild the trace guest without the trace feature to avoid affecting other tests
374+
just build-rust-guests release
375+
just move-rust-guests release
376+
377+
# Fuzzes the guest with tracing enabled. Stops after `max_time` seconds
378+
fuzz-trace-timed max_time fuzz-target="fuzz_guest_trace":
379+
# We need to build the trace guest with the trace feature enabled
380+
just build-rust-guests release trace_guest
381+
just move-rust-guests release
382+
RUST_LOG="trace,hyperlight_guest=trace,hyperlight_guest_bin=trace" cargo +nightly fuzz run {{ fuzz-target }} --features trace --release -- -rss_limit_mb={{ fuzz_memory_limit }} -max_total_time={{ max_time }}
383+
# Rebuild the trace guest without the trace feature to avoid affecting other tests
384+
just build-rust-guests release
385+
just move-rust-guests release
386+
387+
build-trace-fuzzers:
388+
cargo +nightly fuzz build fuzz_guest_trace --features trace
359389

360390
###################
361391
### FLATBUFFERS ###

fuzz/Cargo.toml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ hyperlight-host = { workspace = true, default-features = true, features = ["fuzz
1414
hyperlight-common = {workspace = true}
1515

1616
[[bin]]
17-
name = "fuzz_host_print"
18-
path = "fuzz_targets/host_print.rs"
17+
name = "fuzz_guest_estimate_trace_event"
18+
path = "fuzz_targets/guest_estimate_trace_event.rs"
1919
test = false
2020
doc = false
2121
bench = false
@@ -27,9 +27,27 @@ test = false
2727
doc = false
2828
bench = false
2929

30+
[[bin]]
31+
name = "fuzz_guest_trace"
32+
path = "fuzz_targets/guest_trace.rs"
33+
test = false
34+
doc = false
35+
bench = false
36+
3037
[[bin]]
3138
name = "fuzz_host_call"
3239
path = "fuzz_targets/host_call.rs"
3340
test = false
3441
doc = false
35-
bench = false
42+
bench = false
43+
44+
[[bin]]
45+
name = "fuzz_host_print"
46+
path = "fuzz_targets/host_print.rs"
47+
test = false
48+
doc = false
49+
bench = false
50+
51+
[features]
52+
default = []
53+
trace = ["hyperlight-host/trace_guest", "hyperlight-common/trace_guest"]
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
Copyright 2025 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
#![no_main]
17+
18+
#[cfg(not(feature = "trace"))]
19+
compile_error!("feature `trace` must be enabled to fuzz guest trace event estimation");
20+
21+
use hyperlight_common::flatbuffer_wrappers::guest_trace_data::{
22+
EventKeyValue, EventsBatchDecoder, EventsBatchEncoder, EventsDecoder, EventsEncoder,
23+
GuestEvent, estimate_event,
24+
};
25+
use libfuzzer_sys::arbitrary::{Arbitrary, Result as FuzzResult, Unstructured};
26+
use libfuzzer_sys::fuzz_target;
27+
28+
const MAX_STRING_LEN: usize = 1 << 10; // 1024 bytes
29+
const MAX_FIELDS: usize = 32;
30+
31+
/// Wrapper around GuestEvent to implement Arbitrary
32+
#[derive(Debug)]
33+
struct EventInput(GuestEvent);
34+
35+
impl EventInput {
36+
/// Consumes the wrapper and returns the inner GuestEvent
37+
fn into_inner(self) -> GuestEvent {
38+
self.0
39+
}
40+
}
41+
42+
impl<'a> Arbitrary<'a> for EventInput {
43+
fn arbitrary(u: &mut Unstructured<'a>) -> FuzzResult<Self> {
44+
// Choose a variant of GuestEvent to generate
45+
let discriminator = u.arbitrary::<u8>()? % 5;
46+
47+
// Generate each variant with appropriate random data
48+
let event = match discriminator {
49+
0 => GuestEvent::OpenSpan {
50+
id: u.arbitrary::<u64>()?,
51+
parent_id: arbitrary_parent(u)?,
52+
name: limited_string(u, MAX_STRING_LEN)?,
53+
target: limited_string(u, MAX_STRING_LEN)?,
54+
tsc: u.arbitrary::<u64>()?,
55+
fields: arbitrary_fields(u)?,
56+
},
57+
1 => GuestEvent::CloseSpan {
58+
id: u.arbitrary::<u64>()?,
59+
tsc: u.arbitrary::<u64>()?,
60+
},
61+
2 => GuestEvent::LogEvent {
62+
parent_id: u.arbitrary::<u64>()?,
63+
name: limited_string(u, MAX_STRING_LEN)?,
64+
tsc: u.arbitrary::<u64>()?,
65+
fields: arbitrary_fields(u)?,
66+
},
67+
3 => GuestEvent::EditSpan {
68+
id: u.arbitrary::<u64>()?,
69+
fields: arbitrary_fields(u)?,
70+
},
71+
_ => GuestEvent::GuestStart {
72+
tsc: u.arbitrary::<u64>()?,
73+
},
74+
};
75+
76+
Ok(EventInput(event))
77+
}
78+
}
79+
80+
/// Generates an optional parent ID
81+
fn arbitrary_parent(u: &mut Unstructured<'_>) -> FuzzResult<Option<u64>> {
82+
let has_parent = u.arbitrary::<bool>()?;
83+
if has_parent {
84+
Ok(Some(u.arbitrary::<u64>()?))
85+
} else {
86+
Ok(None)
87+
}
88+
}
89+
90+
/// Generates a String with a maximum length of `max_len`
91+
fn limited_string(u: &mut Unstructured<'_>, max_len: usize) -> FuzzResult<String> {
92+
let bytes = u.arbitrary::<&[u8]>()?;
93+
let s = std::str::from_utf8(bytes)
94+
// Fallback to repeating 'x' if not valid UTF-8
95+
.unwrap_or(&"x".repeat(bytes.len() % max_len))
96+
.chars()
97+
.take(max_len)
98+
.collect::<String>();
99+
100+
Ok(s)
101+
}
102+
103+
/// Generates a vector of EventKeyValue pairs
104+
fn arbitrary_fields(u: &mut Unstructured<'_>) -> FuzzResult<Vec<EventKeyValue>> {
105+
let field_count = (u.arbitrary::<u8>()? as usize).min(MAX_FIELDS);
106+
let mut fields = Vec::with_capacity(field_count);
107+
for _ in 0..field_count {
108+
let key = limited_string(u, MAX_STRING_LEN)?;
109+
let value = limited_string(u, MAX_STRING_LEN)?;
110+
fields.push(EventKeyValue { key, value });
111+
}
112+
Ok(fields)
113+
}
114+
115+
/// Encodes a GuestEvent into a byte vector
116+
fn encode(event: &GuestEvent) -> Vec<u8> {
117+
// Use the estimate plus some slack to avoid reallocation during encoding
118+
let mut encoder = EventsBatchEncoder::new(estimate_event(event).saturating_add(256), |_| {});
119+
encoder.encode(event);
120+
encoder.finish().to_vec()
121+
}
122+
123+
/// Decodes a byte slice into a GuestEvent
124+
fn decode(data: &[u8]) -> Option<GuestEvent> {
125+
let decoder = EventsBatchDecoder {};
126+
let mut events = decoder.decode(data).ok()?;
127+
128+
if events.len() == 1 {
129+
events.pop()
130+
} else {
131+
None
132+
}
133+
}
134+
135+
/// Asserts that the estimated size is within acceptable bounds of the actual size
136+
/// Allows for a 10% slack or minimum of 128 bytes
137+
fn assert_estimate_bounds(actual: usize, estimate: usize) {
138+
assert!(
139+
estimate >= actual,
140+
"estimate {} smaller than actual {}",
141+
estimate,
142+
actual,
143+
);
144+
145+
let slack = (actual / 10).max(128);
146+
let upper_bound = actual + slack;
147+
assert!(
148+
estimate <= upper_bound,
149+
"estimate {} larger than allowable upper bound {} (actual {})",
150+
estimate,
151+
upper_bound,
152+
actual,
153+
);
154+
}
155+
156+
fuzz_target!(|input: EventInput| {
157+
let event = input.into_inner();
158+
159+
// Get the size estimate
160+
let estimate = estimate_event(&event);
161+
// Encode the event
162+
let encoded_data = encode(&event);
163+
// Assert that the estimate is within bounds
164+
assert_estimate_bounds(encoded_data.len(), estimate);
165+
166+
// Decode the event back
167+
let decoded = decode(&encoded_data).expect("decoding failed");
168+
// Assert that the decoded event matches the original
169+
assert_eq!(event, decoded);
170+
});

0 commit comments

Comments
 (0)