Skip to content

Commit 0bed9aa

Browse files
committed
[hyperlight_host/trace] Support collecting guest stacktraces
Signed-off-by: Lucy Menon <[email protected]>
1 parent 76cd801 commit 0bed9aa

File tree

11 files changed

+368
-39
lines changed

11 files changed

+368
-39
lines changed

Cargo.lock

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

src/hyperlight_host/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ workspace = true
2222

2323
[dependencies]
2424
goblin = { version = "0.9" }
25+
framehop = { version = "0.13.1", optional = true }
26+
fallible-iterator = { version = "0.3.0", optional = true }
2527
rand = { version = "0.8.5" }
2628
cfg-if = { version = "1.0.0" }
2729
libc = { version = "0.2.167" }
@@ -127,7 +129,7 @@ crashdump = ["dep:tempfile"] # Dumps the VM state to a file on unexpected errors
127129
trace_guest = []
128130
# This feature enables unwinding the guest stack from the host, in
129131
# order to produce stack traces for debugging or profiling.
130-
unwind_guest = [ "trace_guest" ]
132+
unwind_guest = [ "trace_guest", "dep:framehop", "dep:fallible-iterator" ]
131133
kvm = ["dep:kvm-bindings", "dep:kvm-ioctls"]
132134
mshv = ["dep:mshv-bindings", "dep:mshv-ioctls"]
133135
inprocess = []

src/hyperlight_host/src/hypervisor/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,10 @@ pub(crate) mod tests {
345345
SandboxConfiguration::DEFAULT_MAX_WAIT_FOR_CANCELLATION as u64,
346346
),
347347
#[cfg(feature = "trace_guest")]
348-
trace_info: crate::sandbox::TraceInfo::new()?,
348+
trace_info: crate::sandbox::TraceInfo::new(
349+
#[cfg(feature = "unwind_guest")]
350+
Arc::new(crate::mem::exe::DummyUnwindInfo {})
351+
)?,
349352
};
350353

351354
let mut hv_handler = HypervisorHandler::new(hv_handler_config);

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")]
@@ -23,13 +26,85 @@ use goblin::elf64::program_header::PT_LOAD;
2326

2427
use crate::{log_then_return, new_error, Result};
2528

29+
#[cfg(feature = "unwind_guest")]
30+
struct ResolvedSectionHeader {
31+
name: String,
32+
addr: u64,
33+
offset: u64,
34+
size: u64,
35+
}
36+
2637
pub(crate) struct ElfInfo {
2738
payload: Vec<u8>,
2839
phdrs: ProgramHeaders,
40+
#[cfg(feature = "unwind_guest")]
41+
shdrs: Vec<ResolvedSectionHeader>,
2942
entry: u64,
3043
relocs: Vec<Reloc>,
3144
}
3245

46+
#[cfg(feature = "unwind_guest")]
47+
struct UnwindInfo {
48+
payload: Vec<u8>,
49+
load_addr: u64,
50+
va_size: u64,
51+
base_svma: u64,
52+
shdrs: Vec<ResolvedSectionHeader>,
53+
}
54+
55+
#[cfg(feature = "unwind_guest")]
56+
impl super::exe::UnwindInfo for UnwindInfo {
57+
fn as_module(&self) -> framehop::Module<Vec<u8>> {
58+
framehop::Module::new(
59+
// TODO: plumb through a name from from_file if this
60+
// came from a file
61+
"guest".to_string(),
62+
self.load_addr..self.load_addr + self.va_size,
63+
self.load_addr,
64+
self,
65+
)
66+
}
67+
fn hash(&self) -> blake3::Hash {
68+
blake3::hash(&self.payload)
69+
}
70+
}
71+
72+
#[cfg(feature = "unwind_guest")]
73+
impl UnwindInfo {
74+
fn resolved_section_header(&self, name: &[u8]) -> Option<&ResolvedSectionHeader> {
75+
self.shdrs
76+
.iter()
77+
.find(|&sh| sh.name.as_bytes()[0..core::cmp::min(name.len(), sh.name.len())] == *name)
78+
}
79+
}
80+
81+
#[cfg(feature = "unwind_guest")]
82+
impl framehop::ModuleSectionInfo<Vec<u8>> for &UnwindInfo {
83+
fn base_svma(&self) -> u64 {
84+
self.base_svma
85+
}
86+
fn section_svma_range(&mut self, name: &[u8]) -> Option<std::ops::Range<u64>> {
87+
let shdr = self.resolved_section_header(name)?;
88+
Some(shdr.addr..shdr.addr + shdr.size)
89+
}
90+
fn section_data(&mut self, name: &[u8]) -> Option<Vec<u8>> {
91+
if name == b".eh_frame" && self.resolved_section_header(b".debug_frame").is_some() {
92+
/* Rustc does not always emit enough information for stack
93+
* unwinding in .eh_frame, presumably because we use panic =
94+
* abort in the guest. Framehop defaults to ignoring
95+
* .debug_frame if .eh_frame exists, but we want the opposite
96+
* behaviour here, since .debug_frame will actually contain
97+
* frame information whereas .eh_frame often doesn't because
98+
* of the aforementioned behaviour. Consequently, we hack
99+
* around this by pretending that .eh_frame doesn't exist if
100+
* .debug_frame does. */
101+
return None;
102+
}
103+
let shdr = self.resolved_section_header(name)?;
104+
Some(self.payload[shdr.offset as usize..(shdr.offset + shdr.size) as usize].to_vec())
105+
}
106+
}
107+
33108
impl ElfInfo {
34109
pub(crate) fn new(bytes: &[u8]) -> Result<Self> {
35110
let elf = Elf::parse(bytes)?;
@@ -44,6 +119,19 @@ impl ElfInfo {
44119
Ok(ElfInfo {
45120
payload: bytes.to_vec(),
46121
phdrs: elf.program_headers,
122+
#[cfg(feature = "unwind_guest")]
123+
shdrs: elf
124+
.section_headers
125+
.iter()
126+
.filter_map(|sh| {
127+
Some(ResolvedSectionHeader {
128+
name: elf.shdr_strtab.get_at(sh.sh_name)?.to_string(),
129+
addr: sh.sh_addr,
130+
offset: sh.sh_offset,
131+
size: sh.sh_size,
132+
})
133+
})
134+
.collect(),
47135
entry: elf.entry,
48136
relocs,
49137
})
@@ -68,7 +156,11 @@ impl ElfInfo {
68156
.unwrap(); // guaranteed not to panic because of the check in new()
69157
(max_phdr.p_vaddr + max_phdr.p_memsz - self.get_base_va()) as usize
70158
}
71-
pub(crate) fn load_at(self, load_addr: usize, target: &mut [u8]) -> Result<()> {
159+
pub(crate) fn load_at(
160+
self,
161+
load_addr: usize,
162+
target: &mut [u8],
163+
) -> Result<super::exe::LoadInfo> {
72164
let base_va = self.get_base_va();
73165
for phdr in self.phdrs.iter().filter(|phdr| phdr.p_type == PT_LOAD) {
74166
let start_va = (phdr.p_vaddr - base_va) as usize;
@@ -108,6 +200,20 @@ impl ElfInfo {
108200
}
109201
}
110202
}
111-
Ok(())
203+
cfg_if::cfg_if! {
204+
if #[cfg(feature = "unwind_guest")] {
205+
let va_size = self.get_va_size() as u64;
206+
let base_svma = self.get_base_va();
207+
Ok(Arc::new(UnwindInfo {
208+
payload: self.payload,
209+
load_addr: load_addr as u64,
210+
va_size,
211+
base_svma,
212+
shdrs: self.shdrs,
213+
}))
214+
} else {
215+
Ok(())
216+
}
217+
}
112218
}
113219
}

src/hyperlight_host/src/mem/exe.rs

Lines changed: 45 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;
@@ -40,6 +42,40 @@ pub enum ExeInfo {
4042
const DEFAULT_ELF_STACK_RESERVE: u64 = 65536;
4143
const DEFAULT_ELF_HEAP_RESERVE: u64 = 131072;
4244

45+
#[cfg(feature = "unwind_guest")]
46+
pub(crate) trait UnwindInfo: Send + Sync {
47+
fn as_module(&self) -> framehop::Module<Vec<u8>>;
48+
fn hash(&self) -> blake3::Hash;
49+
}
50+
51+
#[cfg(feature = "unwind_guest")]
52+
pub(crate) struct DummyUnwindInfo {}
53+
#[cfg(feature = "unwind_guest")]
54+
impl UnwindInfo for DummyUnwindInfo {
55+
fn as_module(&self) -> framehop::Module<Vec<u8>> {
56+
framehop::Module::new(
57+
"unsupported".to_string(),
58+
0..0,
59+
0,
60+
self
61+
)
62+
}
63+
fn hash(&self) -> blake3::Hash {
64+
blake3::Hash::from_bytes([0; 32])
65+
}
66+
}
67+
#[cfg(feature = "unwind_guest")]
68+
impl<A> framehop::ModuleSectionInfo<A> for &DummyUnwindInfo {
69+
fn base_svma(&self) -> u64 { 0 }
70+
fn section_svma_range(&mut self, _name: &[u8]) -> Option<std::ops::Range<u64>> { None }
71+
fn section_data(&mut self, _name: &[u8]) -> Option<A> { None }
72+
}
73+
74+
#[cfg(feature = "unwind_guest")]
75+
pub(crate) type LoadInfo = Arc<dyn UnwindInfo>;
76+
#[cfg(not(feature = "unwind_guest"))]
77+
pub(crate) type LoadInfo = ();
78+
4379
impl ExeInfo {
4480
pub fn from_file(path: &str) -> Result<Self> {
4581
let mut file = File::open(path)?;
@@ -80,17 +116,21 @@ impl ExeInfo {
80116
// copying into target, but the PE loader chooses to apply
81117
// relocations in its owned representation of the PE contents,
82118
// which requires it to be &mut.
83-
pub fn load(self, load_addr: usize, target: &mut [u8]) -> Result<()> {
119+
pub fn load(self, load_addr: usize, target: &mut [u8]) -> Result<LoadInfo> {
84120
match self {
85121
ExeInfo::PE(mut pe) => {
86122
let patches = pe.get_exe_relocation_patches(load_addr)?;
87123
pe.apply_relocation_patches(patches)?;
88124
target[0..pe.payload.len()].copy_from_slice(&pe.payload);
125+
cfg_if::cfg_if! {
126+
if #[cfg(feature = "unwind_guest")] {
127+
Ok(Arc::new(DummyUnwindInfo {}))
128+
} else {
129+
Ok(())
130+
}
131+
}
89132
}
90-
ExeInfo::Elf(elf) => {
91-
elf.load_at(load_addr, target)?;
92-
}
133+
ExeInfo::Elf(elf) => elf.load_at(load_addr, target),
93134
}
94-
Ok(())
95135
}
96136
}

src/hyperlight_host/src/mem/layout.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ impl SandboxMemoryLayout {
658658

659659
/// Get the guest address of the code section in the sandbox
660660
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
661-
pub(super) fn get_guest_code_address(&self) -> usize {
661+
pub(crate) fn get_guest_code_address(&self) -> usize {
662662
Self::BASE_ADDRESS + self.guest_code_offset
663663
}
664664

0 commit comments

Comments
 (0)