Debugging
Applications can be debugged by hooking into a debugger, by looking at a backtrace, or by using core files. In SGX, core files can't capture enclave memory and hence can't be used to debug the application running within an enclave.
Debugger
Only debug mode enclaves can be inspected by a debugger.
In order to inspect a debug mode enclave, you need to first attach the debugger to the runner (that is ftxsgx-runner
), then,
supply the debugger with information needed to correctly map the symbols in the enclave's ELF file to the enclave load address.
Once the debugger knows the correct mapping, you need to save the runner's context and switch to the enclave's context.
EDP has simplified these steps by extending GDB with a GDB python script.
Before we get into actual debugging, here are a few terms that will be referred to later:
elf_file
elf_file is the ELF that will be generated by Rust compiler (generally invoked via cargo build
).
Refer to installation_guide for more details on Cargo command-line usage.
You can locate the ELF file from the "cargo run" or "cargo test" output, e.g.:
$ cargo run --target x86_64-fortanix-unknown-sgx
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `ftxsgx-runner-cargo target/x86_64-fortanix-unknown-sgx/debug/sgxtest`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
sgxs_file
The SGXS file is a binary in the processor's native SGXS format which will be loaded by the runner while creating the enclave.
The SGXS file is generated by converting the elf_file. This is normally done automatically when doing cargo run
, but you can refer to the deployment guide for details of the conversion process.
When using cargo build
do not forget to convert an updated build before inspecting it via a debugger.
thread control structure (TCS)
The TCS represents the context of a thread running in the enclave. The context includes the register states, stack information, etc.
In case of an interrupt or asynchronous exit (AEX), the register state will be stored in the state save area (SSA) for the current TCS.
The sgxstate
command from the EDP GDB script interprets the TCS and related state to reconstruct the enclave context.
Getting started with Debugging
Currently, GDB does not support single stepping SGX enclaves. Enclave aborts and processor exceptions can be debugged using gdb.py
with the following steps:
-
By default, the stack is completely unwound after a panic before control reaches the runner, so the debugger can't inspect the panic. In
Cargo.toml
you can configure panics to abort rather than unwind as follows, to preserve the call stack for the debugger:1 2
[profile.dev] panic = "abort"
-
Download gdb.py, if not yet downloaded.
-
Start runner via GDB:
$ gdb --args ftxsgx-runner <path_to_sgxs_file>
You can also attach GDB to a running
ftxsgx-runner
. -
Source
gdb.py
(gdb) source <path_to_gdb.py>
You might want to add the above line in an appropriate
.gdbinit
to avoid repeating this step each time. -
Run until the enclave hits an abort or a processor exception:
(gdb) r
At this point you might see messages that say the program received a SIGTRAP. This is because upon enclave abort or processor exception, the runner sets the TCS address in
rbx
and issuesSIGTRAP
, shuttling control back to GDB. -
Map enclave symbols and switch to enclave state:
(gdb) sgxstate auto <path_to_elf_file>
-
Now you can use standard GDB commands to inspect the enclave stack.
-
To restore the state back to the runner, use:
(gdb) sgxstate restore
-
To switch back to the same thread within the enclave, use:
(gdb) sgxstate auto
This time you do not need to specify the
elf_file
to generate mapping. Be sure you are in the topmost frame of the runner stack, sincesgxstate auto
reads therbx
register to get the TCS address. Or, if the current frame does not have TCS inrbx
, but you know the TCS address, you can use below command to manually switch state:(gdb) sgxstate tcs <TCS ADDR>
Backtrace
As with most of the Rust binaries, panicking will cause a backtrace to be printed on the console if the environment variable RUST_BACKTRACE
is set to 1
.
Since there is no way to inject environment variables from userspace, you need to add code to set the environment variable as follows:
std::env::set_var("RUST_BACKTRACE", "1");
To reduce size of the trusted code base, EDP does not resolve symbols in the backtrace, so the backtrace from an enclave will only print the offsets of the stack address. You can use stack-trace-resolve to resolve the symbols automatically.
A typical usage of stack-trace-resolve
is:
$ cargo +nightly run --target=x86_64-fortanix-unknown-sgx | scripts/stack-trace-resolve -e target/x86_64-fortanix-unknown-sgx/debug/<elf_file>
You can use stack-trace-resolve -h
or refer to stack-trace-resolve info for usage of the script.
You can also use addr2line
to manually resolve the symbols.
Technically, in the SGX threat model the attacker already knows the return addresses in your stack frames, but to make things harder for the attacker, you might not want to set RUST_BACKTRACE = 1
for release builds. Also, you might want to check that the panic message for release builds does not reveal more information than it should.