Skip to content

Commit

Permalink
Support local-exec TLS access
Browse files Browse the repository at this point in the history
This patch enhances OSv dynamic loader to support
pies and position dependant executables that use TLS
(Thread Local Storage) in local-exec mode.

It does so by reserving an extra slot in kernel static
TLS block at its end and designating it as user static TLS
for the executable ELF. Any dependant ELF objects are still
placed in the area before the kernel TLS. For the specifics please
read comments added to arch-elf.cc and arch-switch.hh.

Please note that this solution limits the size of the application
TLS block to 64 bytes plus extra gap due to 64-bytes alignment
of the kernel TLS. This should be sufficient for most applications
which either use tiny TLS (Golang uses 8-bytes long) if at all.
Rust ELFs tend to rely on quite large TLS in which case the limit
in loader.ld needs to be increased accordingly and loader.elf
relinked.

Fixes #352

Signed-off-by: Waldemar Kozaczuk <[email protected]>
  • Loading branch information
wkozaczuk committed Oct 24, 2019
1 parent d44e7c6 commit 70547a6
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 25 deletions.
37 changes: 34 additions & 3 deletions arch/x64/arch-elf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,21 @@ bool object::arch_relocate_rela(u32 type, u32 sym, void *addr,
// target thread-local variable
if (sym) {
auto sm = symbol(sym);
sm.obj->alloc_static_tls();
auto tls_offset = sm.obj->static_tls_end() + sched::kernel_tls_size();
ulong tls_offset;
if (sm.obj->is_executable()) {
tls_offset = sm.obj->get_tls_size();
// If this is an executable (pie or position-dependant one)
// then the variable is located in the reserved slot of the TLS
// right where the kernel TLS lives
// So the offset is negative size of this ELF TLS block
} else {
// If shared library, the variable is located in one of TLS
// blocks that are part of the static TLS before kernel part
// so the offset needs to shift by sum of kernel and size of the user static
// TLS so far
sm.obj->alloc_static_tls();
tls_offset = sm.obj->static_tls_end() + sched::kernel_tls_size();
}
*static_cast<u64*>(addr) = sm.symbol->st_value + addend - tls_offset;
} else {
// TODO: Which case does this handle?
Expand Down Expand Up @@ -166,7 +179,25 @@ void object::prepare_initial_tls(void* buffer, size_t size,
memset(ptr + _tls_init_size, 0, _tls_uninit_size);

offsets.resize(std::max(_module_index + 1, offsets.size()));
offsets[_module_index] = - _static_tls_offset - tls_size - sched::kernel_tls_size();
auto offset = - _static_tls_offset - tls_size - sched::kernel_tls_size();
offsets[_module_index] = offset;
}

void object::prepare_local_tls(std::vector<ptrdiff_t>& offsets)
{
if (!_static_tls && !is_executable()) {
return;
}

offsets.resize(std::max(_module_index + 1, offsets.size()));
auto offset = - get_tls_size();
offsets[_module_index] = offset;
}

void object::copy_local_tls(void* to_addr)
{
memcpy(to_addr, _tls_segment, _tls_init_size); //file size - 48 (0x30) for example and 80 (0x50) for httpserver
memset(to_addr + _tls_init_size, 0, _tls_uninit_size);
}

}
66 changes: 57 additions & 9 deletions arch/x64/arch-switch.hh
Original file line number Diff line number Diff line change
Expand Up @@ -151,33 +151,81 @@ void thread::init_stack()
}

void thread::setup_tcb()
{
assert(tls.size);
{ //
// Most importantly this method allocates TLS memory region and
// sets up TCB (Thread Control Block) that points to that allocated
// memory region. The TLS memory region is designated to a specific thread
// and holds thread local variables (with __thread modifier) defined
// in OSv kernel and the application ELF objects including dependant ones
// through DT_NEEDED tag.
//
// Each ELF object and OSv kernel gets its own TLS block with offsets
// specified in DTV structure (the offsets get calculated as ELF is loaded and symbols
// resolved before we get to this point).
//
// Because both OSv kernel and position-in-dependant (pie) or position-dependant
// executable (non library) are compiled to use local-exec mode to access the thread
// local variables, we need to setup the offsets and TLS blocks in a special way
// to avoid any collisions. Specifically we define OSv TLS segment
// (see arch/x64/loader.ld for specifics) with an extra buffer at
// the end of the kernel TLS to accommodate TLS block of pies and
// position-dependant executables.

// (1) - TLS memory area layout with app shared library
// |-----|-----|-----|--------------|------|
// |SO_3 |SO_2 |SO_1 |KERNEL |<NONE>|
// |-----|-----|-----|--------------|------|

// (2) - TLS memory area layout with pie or
// position dependant executable
// |-----|-----|---------------------|
// |SO_3 |SO_2 |KERNEL | EXE |
// |-----|-----|--------------|------|

assert(sched::tls.size);

void* user_tls_data;
size_t user_tls_size = 0;
size_t executable_tls_size = 0;
if (_app_runtime) {
auto obj = _app_runtime->app.lib();
assert(obj);
user_tls_size = obj->initial_tls_size();
user_tls_data = obj->initial_tls();
if (obj->is_executable()) {
executable_tls_size = obj->get_tls_size();
}
}

// In arch/x64/loader.ld, the TLS template segment is aligned to 64
// bytes, and that's what the objects placed in it assume. So make
// sure our copy is allocated with the same 64-byte alignment, and
// verify that object::init_static_tls() ensured that user_tls_size
// also doesn't break this alignment .
assert(align_check(tls.size, (size_t)64));
// also doesn't break this alignment.
auto kernel_tls_size = sched::tls.size;
assert(align_check(kernel_tls_size, (size_t)64));
assert(align_check(user_tls_size, (size_t)64));
void* p = aligned_alloc(64, sched::tls.size + user_tls_size + sizeof(*_tcb));

auto total_tls_size = kernel_tls_size + user_tls_size;
void* p = aligned_alloc(64, total_tls_size + sizeof(*_tcb));
// First goes user TLS data
if (user_tls_size) {
memcpy(p, user_tls_data, user_tls_size);
}
memcpy(p + user_tls_size, sched::tls.start, sched::tls.filesize);
memset(p + user_tls_size + sched::tls.filesize, 0,
sched::tls.size - sched::tls.filesize);
_tcb = static_cast<thread_control_block*>(p + tls.size + user_tls_size);
// Next goes kernel TLS data
auto kernel_tls_offset = user_tls_size;
memcpy(p + kernel_tls_offset, sched::tls.start, sched::tls.filesize);
memset(p + kernel_tls_offset + sched::tls.filesize, 0,
kernel_tls_size - sched::tls.filesize);

if (executable_tls_size) {
// If executable copy its TLS block data at the designated offset
// at the end of area as described in the ascii art for executables
// TLS layout
auto executable_tls_offset = total_tls_size - executable_tls_size;
_app_runtime->app.lib()->copy_local_tls(p + executable_tls_offset);
}
_tcb = static_cast<thread_control_block*>(p + total_tls_size);
_tcb->self = _tcb;
_tcb->tls_base = p + user_tls_size;

Expand Down
4 changes: 4 additions & 0 deletions arch/x64/loader.ld
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ SECTIONS
.tdata : AT(ADDR(.tdata) - OSV_KERNEL_VM_SHIFT) { *(.tdata .tdata.* .gnu.linkonce.td.*) } :tls :text
.tbss : AT(ADDR(.tbss) - OSV_KERNEL_VM_SHIFT) {
*(.tbss .tbss.* .gnu.linkonce.tb.*)
_pie_static_tls_start = .;
/* This is a reserve intended for executables' (pie or non-pie) TLS block */
. = . + 64;
. = ALIGN(64);
_pie_static_tls_end = .;
} :tls :text
.tls_template_size = SIZEOF(.tdata) + SIZEOF(.tbss);
.bss : AT(ADDR(.bss) - OSV_KERNEL_VM_SHIFT) { *(.bss .bss.*) } :text
Expand Down
21 changes: 16 additions & 5 deletions core/elf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ Elf64_Note::Elf64_Note(void *_base, char *str)
}
}

extern "C" char _pie_static_tls_start, _pie_static_tls_end;
void object::load_segments()
{
for (unsigned i = 0; i < _ehdr.e_phnum; ++i) {
Expand Down Expand Up @@ -471,9 +472,14 @@ void object::load_segments()
// As explained in issue #352, we currently don't correctly support TLS
// used in PIEs.
if (_is_executable && _tls_segment) {
std::cout << "WARNING: " << pathname() << " is a PIE using TLS. This "
<< "is currently unsupported (see issue #352). Link with "
<< "'-shared' instead of '-pie'.\n";
auto tls_size = _tls_init_size + _tls_uninit_size;
ulong pie_static_tls_maximum_size = &_pie_static_tls_end - &_pie_static_tls_start;
if (tls_size > pie_static_tls_maximum_size) {
std::cout << "WARNING: " << pathname() << " is a PIE using TLS of size " << tls_size
<< " which is greater than " << pie_static_tls_maximum_size << " bytes limit. "
<< "Either increase the size of TLS reserve in arch/x64/loader.ld or "
<< "link with '-shared' instead of '-pie'.\n";
}
}
}

Expand Down Expand Up @@ -1110,8 +1116,13 @@ void object::init_static_tls()
if (obj->is_core()) {
continue;
}
obj->prepare_initial_tls(_initial_tls.get(), _initial_tls_size,
_initial_tls_offsets);
if (obj->is_executable()) {
obj->prepare_local_tls(_initial_tls_offsets);
}
else {
obj->prepare_initial_tls(_initial_tls.get(), _initial_tls_size,
_initial_tls_offsets);
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions include/osv/elf.hh
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,12 @@ public:
void init_static_tls();
size_t initial_tls_size() { return _initial_tls_size; }
void* initial_tls() { return _initial_tls.get(); }
void* get_tls_segment() { return _tls_segment; }
bool is_non_pie_executable() { return _ehdr.e_type == ET_EXEC; }
std::vector<ptrdiff_t>& initial_tls_offsets() { return _initial_tls_offsets; }
bool is_executable() { return _is_executable; }
ulong get_tls_size();
void copy_local_tls(void* to_addr);
protected:
virtual void load_segment(const Elf64_Phdr& segment) = 0;
virtual void unload_segment(const Elf64_Phdr& segment) = 0;
Expand All @@ -392,9 +396,9 @@ private:
void relocate_rela();
void relocate_pltgot();
unsigned symtab_len();
ulong get_tls_size();
void collect_dependencies(std::unordered_set<elf::object*>& ds);
void prepare_initial_tls(void* buffer, size_t size, std::vector<ptrdiff_t>& offsets);
void prepare_local_tls(std::vector<ptrdiff_t>& offsets);
void alloc_static_tls();
void make_text_writable(bool flag);
protected:
Expand Down Expand Up @@ -441,7 +445,7 @@ protected:
Elf64_Sxword addend);
bool arch_relocate_jump_slot(u32 sym, void *addr, Elf64_Sxword addend, bool ignore_missing = false);
size_t static_tls_end() {
if (is_core()) {
if (is_core() || is_executable()) {
return 0;
}
return _static_tls_offset + get_tls_size();
Expand Down
14 changes: 12 additions & 2 deletions modules/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \
tst-sendfile.so misc-lock-perf.so tst-uio.so tst-printf.so \
tst-pthread-affinity.so tst-pthread-tsd.so tst-thread-local.so \
tst-zfs-mount.so tst-regex.so tst-tcp-siocoutq.so \
libtls.so tst-tls.so tst-select-timeout.so tst-faccessat.so \
libtls.so tst-tls.so tst-tls-pie.so tst-select-timeout.so tst-faccessat.so \
tst-fstatat.so misc-reboot.so tst-fcntl.so payload-namespace.so \
tst-namespace.so tst-without-namespace.so payload-env.so \
payload-merge-env.so misc-execve.so misc-execve-payload.so misc-mutex2.so \
Expand Down Expand Up @@ -150,7 +150,17 @@ $(out)/tests/tst-tls.so: \
$(src)/tests/tst-tls.cc \
$(out)/tests/libtls.so
$(makedir)
$(call quiet, cd $(out); $(CXX) $(CXXFLAGS) -shared -o $@ $< tests/libtls.so, CXX tst-tls.so)
$(call quiet, cd $(out); $(CXX) $(CXXFLAGS) -D__SHARED_OBJECT__=1 -shared -o $@ $< tests/libtls.so, CXX tst-tls.so)

$(out)/tests/tst-tls-pie.o: CXXFLAGS:=$(subst -fPIC,-fpie,$(CXXFLAGS))
$(out)/tests/tst-tls-pie.o: $(src)/tests/tst-tls.cc
$(makedir)
$(call quiet, $(CXX) $(CXXFLAGS) -c -o $@ $<, CXX $*.cc)
$(out)/tests/tst-tls-pie.so: \
$(out)/tests/tst-tls-pie.o \
$(out)/tests/libtls.so
$(makedir)
$(call quiet, cd $(out); $(CXX) $(CXXFLAGS) -fuse-ld=bfd -pthread -pie -o $@ $< tests/libtls.so, LD tst-tls-pie.so)

boost-tests := tst-vfs.so tst-libc-locking.so misc-fs-stress.so \
misc-bdev-write.so misc-bdev-wlatency.so misc-bdev-rw.so \
Expand Down
7 changes: 7 additions & 0 deletions tests/libtls.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ __thread int ex1 = 321;
__thread int ex2 __attribute__ ((tls_model ("initial-exec"))) = 432;
__thread int ex3 = 765;

extern __thread int v1;
extern __thread int v5;

void external_library()
{
// ex1 and ex3 get accessed by _tls_get_addr()
ex1++;
ex2++;
ex3++;
// These 2 below get handled by get _tls_get_addr() function in core/elf.cc
v1++;
v5++;
}
23 changes: 19 additions & 4 deletions tests/tst-tls.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ static __thread int v6 __attribute__ ((tls_model ("initial-exec"))) = 678;

extern __thread int ex3 __attribute__ ((tls_model ("initial-exec")));

#ifndef __OSV__
#ifndef __SHARED_OBJECT__
// We can also try to force the "Local Exec" TLS model, but OSv's makefile
// builds all tests as shared objects (.so), and the linker will report an
// error, because local-exec is not allowed in shared libraries, just in
// executables (including PIE).
__thread int v7 __attribute__ ((tls_model ("local-exec"))) = 789;
__thread int v8 __attribute__ ((tls_model ("local-exec")));
#endif

extern void external_library();
Expand All @@ -65,14 +66,16 @@ int main(int argc, char** argv)
report(v5 == 567, "v5");
report(v6 == 678, "v6");
report(ex3 == 765, "ex3");
#ifndef __OSV__
#ifndef __SHARED_OBJECT__
report(v7 == 789, "v7");
#endif

external_library();
report(ex1 == 322, "ex1 modified");
report(ex2 == 433, "ex2 modified");
report(ex3 == 766, "ex3 modified");
report(v1 == 124, "v1 modified");
report(v5 == 568, "v5 modified");

// Write on this thread's variables, and see a new thread gets
// the original default values
Expand All @@ -82,7 +85,7 @@ int main(int argc, char** argv)
v4 = 0;
v5 = 0;
v6 = 0;
#ifndef __OSV__
#ifndef __SHARED_OBJECT__
v7 = 0;
#endif

Expand All @@ -97,16 +100,28 @@ int main(int argc, char** argv)
report(v5 == 567, "v5 in new thread");
report(v6 == 678, "v6 in new thread");
report(ex3 == 765, "ex3 in new thread");
#ifndef __OSV__
#ifndef __SHARED_OBJECT__
report(v7 == 789, "v7 in new thread");
#endif

external_library();
report(ex1 == 322, "ex1 modified in new thread");
report(ex2 == 433, "ex2 modified in new thread");
report(ex3 == 766, "ex3 modified in new thread");
report(v1 == 124, "v1 modified in new thread");
report(v5 == 568, "v5 modified");
});
t1.join();

std::cout << "SUMMARY: " << tests << " tests, " << fails << " failures\n";
}

#ifndef __SHARED_OBJECT__
static void before_main(void) __attribute__((constructor));
static void before_main(void)
{
report(v7 == 789, "v7 in init function");
report(v8 == 0, "v8 in init function");
}
#endif

0 comments on commit 70547a6

Please sign in to comment.