Re: [PATCH 2/5] selftests/sgx: Fix function pointer relocation in test enclave.

From: Jo Van Bulck
Date: Mon Aug 07 2023 - 03:14:01 EST


On 03.08.23 05:58, Huang, Kai wrote:
Putting aside whether we should consider building the selftests using "-Os", it
would be helpful to explain how can the "-Os" break the existing code, so that
we can review why this fix is reasonable. Perhaps it's obvious to others but
it's not obvious to me what can go wrong here.

I dug deeper into this and the real problem here is that the enclave ELF is linked with -static but also needs to be relocatable, which, in my understanding, is not what -static is meant for (i.e., the man pages says -static overrides -pie). Particularly, with -static I noticed that global variables are hard-coded assuming the ELF is loaded at base address zero.

When reading more on this, it seems like the proper thing to do for static and relocatable binaries is to compile with -static-pie, which is added since gcc 8 [1] (and similarly supported by clang).

As a case in point, to hopefully make this clearer, consider the following C function:

extern uint8_t __enclave_base;
void *get_base(void) {
return &__enclave_base;
}

Compiling with -static and -fPIC hard codes the __enclave_base symbol to zero (the start of the ELF enclave image):

00000000000023f4 <get_base>:
23f4: f3 0f 1e fa endbr64
23f8: 55 push %rbp
23f9: 48 89 e5 mov %rsp,%rbp
23fc: 48 c7 c0 00 00 00 00 mov $0x0,%rax
2403: 5d pop %rbp
2404: c3 ret

Compiling with -static-pie and -fPIE properly emits a RIP-relative address:

00000000000023f4 <get_base>:
23f4: f3 0f 1e fa endbr64
23f8: 55 push %rbp
23f9: 48 89 e5 mov %rsp,%rbp
23fc: 48 8d 05 fd db ff ff lea -0x2403(%rip),%rax # 0 <__enclave_base>
2403: 5d pop %rbp
2404: c3 ret

Now, the fact that it currently *happens* to work is a mere coincidence of how the local encl_op_array initialization is compiled without optimizations with -static -fPIC:

00000000000023f4 <encl_body>:
/* snipped */
2408: 48 8d 05 ec fe ff ff lea -0x114(%rip),%rax # 22fb <do_encl_op_put_to_buf>
240f: 48 89 45 b0 mov %rax,-0x50(%rbp)
2413: 48 8d 05 18 ff ff ff lea -0xe8(%rip),%rax # 2332 <do_encl_op_get_from_buf>
241a: 48 89 45 b8 mov %rax,-0x48(%rbp)
241e: 48 8d 05 44 ff ff ff lea -0xbc(%rip),%rax # 2369 <do_encl_op_put_to_addr>
/* snipped */

When compiling with optimizations with -static -fPIC -Os, encl_op_array is instead initialized with a prepared copy from .data:

00000000000021b5 <encl_body>:
/* snipped */
21bc: 48 8d 35 3d 2e 00 00 lea 0x2e3d(%rip),%rsi # 5000 <encl_buffer+0x2000>
21c3: 48 8d 7c 24 b8 lea -0x48(%rsp),%rdi
21c8: b9 10 00 00 00 mov $0x10,%ecx
21cd: f3 a5 rep movsl %ds:(%rsi),%es:(%rdi)
/* snipped */

Thus, in this optimized code, encl_op_array will have function pointers that are *not* relocated. The compilation assumes the -static binary has base address zero and is not relocatable:

$ readelf -r test_encl.elf

There are no relocations in this file.

When compiling with -static-pie -PIE -Os, the same code is emitted *but* the binary is relocatable:

$ readelf -r test_encl.elf

Relocation section '.rela.dyn' at offset 0x4000 contains 12 entries:
Offset Info Type Sym. Value Sym. Name + Addend
# tcs1.ossa
000000000010 000000000008 R_X86_64_RELATIVE 7000
# tcs1.oentry
000000000020 000000000008 R_X86_64_RELATIVE 21e3
# tcs2.ossa
000000001010 000000000008 R_X86_64_RELATIVE 8000
# tcs2.oentry
000000001020 000000000008 R_X86_64_RELATIVE 21e3
# encl_op_array
000000006000 000000000008 R_X86_64_RELATIVE 2098
000000006008 000000000008 R_X86_64_RELATIVE 20ae
000000006010 000000000008 R_X86_64_RELATIVE 20c4
000000006018 000000000008 R_X86_64_RELATIVE 20d7
000000006020 000000000008 R_X86_64_RELATIVE 20ea
000000006028 000000000008 R_X86_64_RELATIVE 203e
000000006030 000000000008 R_X86_64_RELATIVE 2000
000000006038 000000000008 R_X86_64_RELATIVE 20ef

Apparently, for static-pie binaries, glibc includes a _dl_relocate_static_pie routine that will perform the relocations as part of the startup [2,3]. Since the enclave loading process is different and glibc is not included, we cannot rely on these relocations to be performed and we need to do them manually. Note: the first 4 symbols in the relocation table above are from the TCS initialization in test_encl_bootstrap.S and should *not* be relocated (as these are relative to enclave base as per SGX spec).

Bottom line: the way I see it, the enclave should either ensure no relocations are needed, or perform the relocations manually where needed, or include a _dl_relocate_static_pie equivalent that parses the .rela.dyn ELF section and patches all relocations (minus the TCS symbols). Since the latter (most general) approach is likely going to make the selftest enclave unnecessarily complex by including ELF parsing etc, I propose to simply relocate the function-pointer table manually (which is indeed the only place that needs relocations).

I will include code to properly compile the selftest enclave with -static-pie as per above and relocate the function-pointer table in the next patch revision.

[1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81498
[2] https://stackoverflow.com/a/62587156
[3] https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/dl-reloc-static-pie.c;h=382482fa739f10934aeb916650c65a4db231c481;hb=HEAD