Join our public slack channel for support, discussions and more...
Contents

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 gudie 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.

Currently, GDB does not support single stepping SGX enclaves. Enclave aborts and processor exceptions can be debugged using gdb.py with the following steps:

  1. Download gdb.py, if not yet downloaded.

  2. Start runner via GDB:

    $ gdb --args ftxsgx-runner <path_to_sgxs_file>
    

    You can also attach GDB to a running ftxsgx-runner.

  3. 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.

  4. 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 issues SIGTRAP, shuttling control back to GDB.

  5. Map enclave symbols and switch to enclave state:

    (gdb) sgxstate auto <path_to_elf_file>
    
  6. Now you can use standard GDB commands to inspect the enclave stack.

  7. To restore the state back to the runner, use:

    (gdb) sgxstate restore
    
  8. To switch back to the same thread within the enclave, use:

    (gdb) sgxstate auto
    

    This time you do not need not specify the elf_file to generate mapping. Be sure that you are in the topmost frame of the runner stack, since sgxstate auto reads the rbx register to get the TCS address. Or, if the current frame does not have TCS in rbx, but you know the TCS address, you can use below command to manually switch state:

    (gdb) sgxstate tcs <TCS ADDR>
    
  9. 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"
    

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.

Security warning

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.

Contents