Skip to content

Commit b0275a9

Browse files
syntacticallydblnz
authored andcommitted
[hyperlight_host/trace] Support collecting guest stacktraces
Signed-off-by: Lucy Menon <[email protected]> Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent 896fbea commit b0275a9

File tree

12 files changed

+614
-335
lines changed

12 files changed

+614
-335
lines changed

Cargo.lock

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

src/hyperlight_common/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ spin = "0.10.0"
2525
[features]
2626
default = ["tracing"]
2727
fuzzing = ["dep:arbitrary"]
28+
unwind_guest = []
2829
std = []
2930

3031
[dev-dependencies]
3132
hyperlight-testing = { workspace = true }
3233

3334
[lib]
3435
bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
35-
doctest = false # reduce noise in test output
36+
doctest = false # reduce noise in test output

src/hyperlight_common/src/outb.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ impl TryFrom<u8> for Exception {
9090
/// - CallFunction: makes a call to a host function,
9191
/// - Abort: aborts the execution of the guest,
9292
/// - DebugPrint: prints a message to the host
93+
/// - TraceRecordStack: records the stack trace of the guest
9394
pub enum OutBAction {
9495
Log = 99,
9596
CallFunction = 101,
9697
Abort = 102,
9798
DebugPrint = 103,
99+
#[cfg(feature = "unwind_guest")]
100+
TraceRecordStack = 104,
98101
}
99102

100103
impl TryFrom<u16> for OutBAction {
@@ -105,6 +108,8 @@ impl TryFrom<u16> for OutBAction {
105108
101 => Ok(OutBAction::CallFunction),
106109
102 => Ok(OutBAction::Abort),
107110
103 => Ok(OutBAction::DebugPrint),
111+
#[cfg(feature = "unwind_guest")]
112+
104 => Ok(OutBAction::TraceRecordStack),
108113
_ => Err(anyhow::anyhow!("Invalid OutBAction value: {}", val)),
109114
}
110115
}

src/hyperlight_host/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ rand = { version = "0.9" }
2828
cfg-if = { version = "1.0.1" }
2929
libc = { version = "0.2.174" }
3030
flatbuffers = "25.2.10"
31+
framehop = { version = "0.13.1", optional = true }
32+
fallible-iterator = { version = "0.3.0", optional = true }
3133
page_size = "0.6.0"
3234
termcolor = "1.2.0"
3335
bitflags = "2.9.1"
@@ -45,6 +47,7 @@ metrics = "0.24.2"
4547
serde_json = "1.0"
4648
elfcore = "2.0"
4749
uuid = { version = "1.17.0", features = ["v4"] }
50+
blake3 = "1.8.2"
4851

4952
[target.'cfg(windows)'.dependencies]
5053
windows = { version = "0.61", features = [
@@ -131,7 +134,7 @@ crashdump = ["dep:chrono"]
131134
trace_guest = []
132135
# This feature enables unwinding the guest stack from the host, in
133136
# order to produce stack traces for debugging or profiling.
134-
unwind_guest = [ "trace_guest" ]
137+
unwind_guest = [ "trace_guest", "dep:framehop", "dep:fallible-iterator", "hyperlight-common/unwind_guest" ]
135138
kvm = ["dep:kvm-bindings", "dep:kvm-ioctls"]
136139
mshv2 = ["dep:mshv-bindings2", "dep:mshv-ioctls2"]
137140
mshv3 = ["dep:mshv-bindings3", "dep:mshv-ioctls3"]

src/hyperlight_host/src/hypervisor/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ pub struct VirtualCPU {}
269269
impl VirtualCPU {
270270
/// Run the given hypervisor until a halt instruction is reached
271271
#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
272-
pub fn run(
272+
pub(crate) fn run(
273273
hv: &mut dyn Hypervisor,
274274
outb_handle_fn: Arc<Mutex<dyn OutBHandlerCaller>>,
275275
mem_access_fn: Arc<Mutex<dyn MemAccessHandlerCaller>>,

src/hyperlight_host/src/mem/elf.rs

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
#[cfg(feature = "unwind_guest")]
18+
use std::sync::Arc;
19+
1720
#[cfg(target_arch = "aarch64")]
1821
use goblin::elf::reloc::{R_AARCH64_NONE, R_AARCH64_RELATIVE};
1922
#[cfg(target_arch = "x86_64")]
@@ -26,13 +29,85 @@ use goblin::elf64::program_header::PT_LOAD;
2629

2730
use crate::{Result, log_then_return, new_error};
2831

32+
#[cfg(feature = "unwind_guest")]
33+
struct ResolvedSectionHeader {
34+
name: String,
35+
addr: u64,
36+
offset: u64,
37+
size: u64,
38+
}
39+
2940
pub(crate) struct ElfInfo {
3041
payload: Vec<u8>,
3142
phdrs: ProgramHeaders,
43+
#[cfg(feature = "unwind_guest")]
44+
shdrs: Vec<ResolvedSectionHeader>,
3245
entry: u64,
3346
relocs: Vec<Reloc>,
3447
}
3548

49+
#[cfg(feature = "unwind_guest")]
50+
struct UnwindInfo {
51+
payload: Vec<u8>,
52+
load_addr: u64,
53+
va_size: u64,
54+
base_svma: u64,
55+
shdrs: Vec<ResolvedSectionHeader>,
56+
}
57+
58+
#[cfg(feature = "unwind_guest")]
59+
impl super::exe::UnwindInfo for UnwindInfo {
60+
fn as_module(&self) -> framehop::Module<Vec<u8>> {
61+
framehop::Module::new(
62+
// TODO: plumb through a name from from_file if this
63+
// came from a file
64+
"guest".to_string(),
65+
self.load_addr..self.load_addr + self.va_size,
66+
self.load_addr,
67+
self,
68+
)
69+
}
70+
fn hash(&self) -> blake3::Hash {
71+
blake3::hash(&self.payload)
72+
}
73+
}
74+
75+
#[cfg(feature = "unwind_guest")]
76+
impl UnwindInfo {
77+
fn resolved_section_header(&self, name: &[u8]) -> Option<&ResolvedSectionHeader> {
78+
self.shdrs
79+
.iter()
80+
.find(|&sh| sh.name.as_bytes()[0..core::cmp::min(name.len(), sh.name.len())] == *name)
81+
}
82+
}
83+
84+
#[cfg(feature = "unwind_guest")]
85+
impl framehop::ModuleSectionInfo<Vec<u8>> for &UnwindInfo {
86+
fn base_svma(&self) -> u64 {
87+
self.base_svma
88+
}
89+
fn section_svma_range(&mut self, name: &[u8]) -> Option<std::ops::Range<u64>> {
90+
let shdr = self.resolved_section_header(name)?;
91+
Some(shdr.addr..shdr.addr + shdr.size)
92+
}
93+
fn section_data(&mut self, name: &[u8]) -> Option<Vec<u8>> {
94+
if name == b".eh_frame" && self.resolved_section_header(b".debug_frame").is_some() {
95+
/* Rustc does not always emit enough information for stack
96+
* unwinding in .eh_frame, presumably because we use panic =
97+
* abort in the guest. Framehop defaults to ignoring
98+
* .debug_frame if .eh_frame exists, but we want the opposite
99+
* behaviour here, since .debug_frame will actually contain
100+
* frame information whereas .eh_frame often doesn't because
101+
* of the aforementioned behaviour. Consequently, we hack
102+
* around this by pretending that .eh_frame doesn't exist if
103+
* .debug_frame does. */
104+
return None;
105+
}
106+
let shdr = self.resolved_section_header(name)?;
107+
Some(self.payload[shdr.offset as usize..(shdr.offset + shdr.size) as usize].to_vec())
108+
}
109+
}
110+
36111
impl ElfInfo {
37112
pub(crate) fn new(bytes: &[u8]) -> Result<Self> {
38113
let elf = Elf::parse(bytes)?;
@@ -47,6 +122,19 @@ impl ElfInfo {
47122
Ok(ElfInfo {
48123
payload: bytes.to_vec(),
49124
phdrs: elf.program_headers,
125+
#[cfg(feature = "unwind_guest")]
126+
shdrs: elf
127+
.section_headers
128+
.iter()
129+
.filter_map(|sh| {
130+
Some(ResolvedSectionHeader {
131+
name: elf.shdr_strtab.get_at(sh.sh_name)?.to_string(),
132+
addr: sh.sh_addr,
133+
offset: sh.sh_offset,
134+
size: sh.sh_size,
135+
})
136+
})
137+
.collect(),
50138
entry: elf.entry,
51139
relocs,
52140
})
@@ -73,7 +161,11 @@ impl ElfInfo {
73161
.unwrap();
74162
(max_phdr.p_vaddr + max_phdr.p_memsz - self.get_base_va()) as usize
75163
}
76-
pub(crate) fn load_at(&mut self, load_addr: usize, target: &mut [u8]) -> Result<()> {
164+
pub(crate) fn load_at(
165+
self,
166+
load_addr: usize,
167+
target: &mut [u8],
168+
) -> Result<super::exe::LoadInfo> {
77169
let base_va = self.get_base_va();
78170
for phdr in self.phdrs.iter().filter(|phdr| phdr.p_type == PT_LOAD) {
79171
let start_va = (phdr.p_vaddr - base_va) as usize;
@@ -113,6 +205,20 @@ impl ElfInfo {
113205
}
114206
}
115207
}
116-
Ok(())
208+
cfg_if::cfg_if! {
209+
if #[cfg(feature = "unwind_guest")] {
210+
let va_size = self.get_va_size() as u64;
211+
let base_svma = self.get_base_va();
212+
Ok(Arc::new(UnwindInfo {
213+
payload: self.payload,
214+
load_addr: load_addr as u64,
215+
va_size,
216+
base_svma,
217+
shdrs: self.shdrs,
218+
}))
219+
} else {
220+
Ok(())
221+
}
222+
}
117223
}
118224
}

src/hyperlight_host/src/mem/exe.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ limitations under the License.
1616

1717
use std::fs::File;
1818
use std::io::Read;
19+
#[cfg(feature = "unwind_guest")]
20+
use std::sync::Arc;
1921
use std::vec::Vec;
2022

2123
use super::elf::ElfInfo;
@@ -37,6 +39,41 @@ pub enum ExeInfo {
3739
const DEFAULT_ELF_STACK_RESERVE: u64 = 65536;
3840
const DEFAULT_ELF_HEAP_RESERVE: u64 = 131072;
3941

42+
#[cfg(feature = "unwind_guest")]
43+
pub(crate) trait UnwindInfo: Send + Sync {
44+
fn as_module(&self) -> framehop::Module<Vec<u8>>;
45+
fn hash(&self) -> blake3::Hash;
46+
}
47+
48+
#[cfg(feature = "unwind_guest")]
49+
pub(crate) struct DummyUnwindInfo {}
50+
#[cfg(feature = "unwind_guest")]
51+
impl UnwindInfo for DummyUnwindInfo {
52+
fn as_module(&self) -> framehop::Module<Vec<u8>> {
53+
framehop::Module::new("unsupported".to_string(), 0..0, 0, self)
54+
}
55+
fn hash(&self) -> blake3::Hash {
56+
blake3::Hash::from_bytes([0; 32])
57+
}
58+
}
59+
#[cfg(feature = "unwind_guest")]
60+
impl<A> framehop::ModuleSectionInfo<A> for &DummyUnwindInfo {
61+
fn base_svma(&self) -> u64 {
62+
0
63+
}
64+
fn section_svma_range(&mut self, _name: &[u8]) -> Option<std::ops::Range<u64>> {
65+
None
66+
}
67+
fn section_data(&mut self, _name: &[u8]) -> Option<A> {
68+
None
69+
}
70+
}
71+
72+
#[cfg(feature = "unwind_guest")]
73+
pub(crate) type LoadInfo = Arc<dyn UnwindInfo>;
74+
#[cfg(not(feature = "unwind_guest"))]
75+
pub(crate) type LoadInfo = ();
76+
4077
impl ExeInfo {
4178
pub fn from_file(path: &str) -> Result<Self> {
4279
let mut file = File::open(path)?;
@@ -71,12 +108,9 @@ impl ExeInfo {
71108
// copying into target, but the PE loader chooses to apply
72109
// relocations in its owned representation of the PE contents,
73110
// which requires it to be &mut.
74-
pub fn load(self, load_addr: usize, target: &mut [u8]) -> Result<()> {
111+
pub fn load(self, load_addr: usize, target: &mut [u8]) -> Result<LoadInfo> {
75112
match self {
76-
ExeInfo::Elf(mut elf) => {
77-
elf.load_at(load_addr, target)?;
78-
}
113+
ExeInfo::Elf(elf) => elf.load_at(load_addr, target),
79114
}
80-
Ok(())
81115
}
82116
}

src/hyperlight_host/src/mem/mgr.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
338338
cfg: SandboxConfiguration,
339339
exe_info: ExeInfo,
340340
guest_blob: Option<&GuestBlob>,
341-
) -> Result<Self> {
341+
) -> Result<(Self, super::exe::LoadInfo)> {
342342
let guest_blob_size = guest_blob.map(|b| b.data.len()).unwrap_or(0);
343343
let guest_blob_mem_flags = guest_blob.map(|b| b.permissions);
344344

@@ -364,18 +364,21 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
364364
shared_mem.write_u64(offset, load_addr_u64)?;
365365
}
366366

367-
exe_info.load(
367+
let load_info = exe_info.load(
368368
load_addr.clone().try_into()?,
369369
&mut shared_mem.as_mut_slice()[layout.get_guest_code_offset()..],
370370
)?;
371371

372-
Ok(Self::new(
373-
layout,
374-
shared_mem,
375-
load_addr,
376-
entrypoint_offset,
377-
#[cfg(target_os = "windows")]
378-
None,
372+
Ok((
373+
Self::new(
374+
layout,
375+
shared_mem,
376+
load_addr,
377+
entrypoint_offset,
378+
#[cfg(target_os = "windows")]
379+
None,
380+
),
381+
load_info,
379382
))
380383
}
381384

0 commit comments

Comments
 (0)