WIP add support for GNU LibC TLS model

Unlike on Windows, TLS index isn't part of the module format. glibc stores
it in the opaque link_map struct. There are many different heuristics
a debugger can use to locate it but most of them are unreliable and
prone to errors, or don't work when debugger attaches to a process.
We adopt the newer approach of looking for special symbols
in glibc that provide TLS index offset. These symbols were added in
2021, so programs linked with older glibc won't have TLS support.
This commit is contained in:
Nikita Smith
2025-11-21 16:41:35 -08:00
parent 7347d2f1bc
commit 0565fdef0a
8 changed files with 361 additions and 238 deletions
+14
View File
@@ -18,6 +18,17 @@ struct DMN_CtrlCtx
U64 u64[1];
};
////////////////////////////////
//~ Dynamic Linker Types
typedef U32 DMN_TlsModel;
enum
{
DMN_TlsModel_Null,
DMN_TlsModel_WinodwsNt,
DMN_TlsModel_Gnu
};
////////////////////////////////
//~ rjf: Handle Types
@@ -82,6 +93,9 @@ struct DMN_Event
U64 stack_pointer;
U64 user_data;
B32 exception_repeated;
U64 tls_index;
U64 tls_offset;
DMN_TlsModel tls_model;
};
typedef struct DMN_EventNode DMN_EventNode;
+124 -63
View File
@@ -1401,6 +1401,42 @@ dmn_lnx_handle_not_attached(Arena *arena, DMN_EventList *events)
e->error_kind = DMN_ErrorKind_NotAttached;
}
internal DMN_LNX_Entity *
dmn_lnx_handle_create_thread(Arena *arena, DMN_EventList *events, DMN_LNX_Entity *process, pid_t tid)
{
DMN_LNX_Entity *thread = dmn_lnx_entity_alloc(process, DMN_LNX_EntityKind_Thread);
thread->id = tid;
thread->arch = process->arch;
thread->reg_block = push_array(process->arena, U8, regs_block_size_from_arch(process->arch));
thread->thread_state = DMN_LNX_ThreadState_Stopped;
thread->is_reg_block_dirty = !dmn_lnx_thread_read_reg_block(thread);
hash_table_push_u64_raw(dmn_lnx_state->arena, dmn_lnx_state->tid_ht, thread->id, thread);
if(!thread->is_reg_block_dirty)
{
switch(thread->arch)
{
case Arch_Null: break;
case Arch_x64: thread->thread_local_base = ((REGS_RegBlockX64 *)thread->reg_block)->fsbase.u64; break;
case Arch_x86:
case Arch_arm32:
case Arch_arm64: { NotImplemented; } break;
default: { InvalidPath; } break;
}
}
DMN_Event *e = dmn_event_list_push(arena, events);
e->kind = DMN_EventKind_CreateThread;
e->process = dmn_lnx_handle_from_entity(process);
e->thread = dmn_lnx_handle_from_entity(thread);
e->arch = thread->arch;
e->code = thread->id;
process->thread_count += 1;
return thread;
}
internal void
dmn_lnx_handle_create_process(Arena *arena, DMN_EventList *events, pid_t pid)
{
@@ -1529,32 +1565,17 @@ dmn_lnx_handle_create_process(Arena *arena, DMN_EventList *events, pid_t pid)
// push create process event
{
DMN_Event *e = dmn_event_list_push(arena, events);
e->kind = DMN_EventKind_CreateProcess;
e->process = dmn_lnx_handle_from_entity(process);
e->arch = process->arch;
e->code = pid;
e->kind = DMN_EventKind_CreateProcess;
e->process = dmn_lnx_handle_from_entity(process);
e->arch = process->arch;
e->code = pid;
e->tls_model = DMN_TlsModel_Gnu; // TODO: use dynamic linker path to figure out correct enum here
}
//
// init main thread
//
DMN_LNX_Entity *thread = dmn_lnx_entity_alloc(process, DMN_LNX_EntityKind_Thread);
thread->id = pid;
thread->arch = process->arch;
thread->is_main_thread = 1;
thread->thread_state = DMN_LNX_ThreadState_Stopped;
thread->reg_block = push_array(process->arena, U8, regs_block_size_from_arch(process->arch));
thread->is_reg_block_dirty = !dmn_lnx_thread_read_reg_block(thread);
hash_table_push_u64_raw(dmn_lnx_state->arena, dmn_lnx_state->tid_ht, thread->id, thread);
// push create thread event
{
DMN_Event *e = dmn_event_list_push(arena, events);
e->kind = DMN_EventKind_CreateThread;
e->process = dmn_lnx_handle_from_entity(process);
e->thread = dmn_lnx_handle_from_entity(thread);
e->arch = thread->arch;
e->code = thread->id;
}
DMN_LNX_Entity *thread = dmn_lnx_handle_create_thread(arena, events, process, pid);
//
// init main module
@@ -1580,6 +1601,8 @@ dmn_lnx_handle_create_process(Arena *arena, DMN_EventList *events, pid_t pid)
e->string = dmn_lnx_read_string(arena, process->fd, auxv.execfn);
e->elf_phdr_vrange = r1u64(auxv.phdr, auxv.phdr + auxv.phent * auxv.phnum);
e->elf_phdr_entsize = auxv.phent;
e->tls_index = 1;
e->tls_offset = 0;
}
}
@@ -1643,30 +1666,6 @@ dmn_lnx_handle_exit_process(Arena *arena, DMN_EventList *events, pid_t pid)
dmn_lnx_state->active_process_count -= 1;
}
internal void
dmn_lnx_handle_create_thread(Arena *arena, DMN_EventList *events, pid_t tid, pid_t new_tid)
{
DMN_LNX_Entity *thread = dmn_lnx_thread_from_pid(tid);
DMN_LNX_Entity *process = thread->parent;
DMN_LNX_Entity *new_thread = dmn_lnx_entity_alloc(process, DMN_LNX_EntityKind_Thread);
new_thread->id = new_tid;
new_thread->arch = process->arch;
new_thread->reg_block = push_array(process->arena, U8, regs_block_size_from_arch(process->arch));
new_thread->thread_state = DMN_LNX_ThreadState_Stopped;
new_thread->is_reg_block_dirty = !dmn_lnx_thread_read_reg_block(new_thread);
hash_table_push_u64_raw(dmn_lnx_state->arena, dmn_lnx_state->tid_ht, new_thread->id, new_thread);
DMN_Event *e = dmn_event_list_push(arena, events);
e->kind = DMN_EventKind_CreateThread;
e->process = dmn_lnx_handle_from_entity(process);
e->thread = dmn_lnx_handle_from_entity(new_thread);
e->arch = new_thread->arch;
e->code = new_thread->id;
process->thread_count += 1;
}
internal void
dmn_lnx_handle_exit_thread(Arena *arena, DMN_EventList *events, pid_t tid, U64 exit_code)
{
@@ -1716,14 +1715,27 @@ dmn_lnx_handle_load_module(Arena *arena, DMN_EventList *events, DMN_LNX_Entity *
DMN_LNX_PhdrInfo module_phdr_info = dmn_lnx_phdr_info_from_memory(process->fd, module_ehdr.e_ident[ELF_Identifier_Class], module_rebase, module_phdr_vaddr, module_ehdr.e_phentsize, module_ehdr.e_phnum);
String8 module_name = dmn_lnx_read_string(process->arena, process->fd, map.name_vaddr);
// read TLS index and TLS offset
U64 tls_index = max_U64;
U64 tls_offset = max_U64;
if(dmn_lnx_state->is_tls_detected)
{
Rng1U64 tls_modid_range = r1u64(dmn_lnx_state->tls_modid_desc.offset, dmn_lnx_state->tls_modid_desc.offset + dmn_lnx_state->tls_modid_desc.bit_size / 8);
Rng1U64 tls_offset_range = r1u64(dmn_lnx_state->tls_offset_desc.offset, dmn_lnx_state->tls_offset_desc.offset + dmn_lnx_state->tls_offset_desc.bit_size / 8);
tls_modid_range = shift_1u64(tls_modid_range, map_vaddr);
tls_offset_range = shift_1u64(tls_offset_range, map_vaddr);
if(!dmn_lnx_read(process->fd, tls_modid_range, &tls_index)) { Assert(0 && "failed to read TLS index"); }
if(!dmn_lnx_read(process->fd, tls_offset_range, &tls_offset)) { Assert(0 && "failed to read TLS offset"); }
}
// fill out module
module = dmn_lnx_entity_alloc(process, DMN_LNX_EntityKind_Module);
module->id = map.name_vaddr;
module->base_vaddr = map.addr_vaddr;
module = dmn_lnx_entity_alloc(process, DMN_LNX_EntityKind_Module);
module->id = map.name_vaddr;
module->base_vaddr = map.addr_vaddr;
// push load event
if(!str8_match(module_name, str8_lit("linux-vdso.so.1"), 0))
{
// push load event
DMN_Event *e = dmn_event_list_push(arena, events);
e->kind = DMN_EventKind_LoadModule;
e->process = dmn_lnx_handle_from_entity(process);
@@ -1734,6 +1746,8 @@ dmn_lnx_handle_load_module(Arena *arena, DMN_EventList *events, DMN_LNX_Entity *
e->string = module_name;
e->elf_phdr_vrange = r1u64(module_phdr_vaddr, module_phdr_vaddr + module_ehdr.e_phentsize * module_ehdr.e_phnum);
e->elf_phdr_entsize = module_ehdr.e_phentsize;
e->tls_index = tls_index;
e->tls_offset = tls_offset;
}
// create mapping for base -> module
@@ -2007,19 +2021,40 @@ internal void
dmn_init(void)
{
local_persist B32 was_inited;
local_persist DMN_LNX_State state;
AssertAlways(!was_inited);
was_inited = 1;
if(!was_inited)
{
was_inited = 1;
dmn_lnx_state = &state;
dmn_lnx_state->arena = arena_alloc();
dmn_lnx_state->access_mutex = mutex_alloc();
dmn_lnx_state->entities_arena = arena_alloc(.reserve_size = GB(32), .commit_size = KB(64), .flags = ArenaFlag_NoChain);
dmn_lnx_state->entities_base = push_array(dmn_lnx_state->entities_arena, DMN_LNX_Entity, 0);
dmn_lnx_state->tid_ht = hash_table_init(dmn_lnx_state->arena, 0x2000);
dmn_lnx_state->pid_ht = hash_table_init(dmn_lnx_state->arena, 0x400);
dmn_lnx_state->halter_mutex = mutex_alloc();
dmn_lnx_entity_alloc(dmn_lnx_nil_entity, DMN_LNX_EntityKind_Root);
local_persist DMN_LNX_State state;
dmn_lnx_state = &state;
dmn_lnx_state->arena = arena_alloc();
dmn_lnx_state->access_mutex = mutex_alloc();
dmn_lnx_state->entities_arena = arena_alloc(.reserve_size = GB(32), .commit_size = KB(64), .flags = ArenaFlag_NoChain);
dmn_lnx_state->entities_base = push_array(dmn_lnx_state->entities_arena, DMN_LNX_Entity, 0);
dmn_lnx_state->tid_ht = hash_table_init(dmn_lnx_state->arena, 0x2000);
dmn_lnx_state->pid_ht = hash_table_init(dmn_lnx_state->arena, 0x400);
dmn_lnx_state->halter_mutex = mutex_alloc();
dmn_lnx_entity_alloc(dmn_lnx_nil_entity, DMN_LNX_EntityKind_Root);
// find offsets of TLS index and TLS offset in the link_map struct
//
// TODO: assuming that target is using same libc version as debugger
{
DMN_LNX_DbDesc *tls_modid_desc = dlsym(RTLD_DEFAULT, "_thread_db_link_map_l_tls_modid");
DMN_LNX_DbDesc *tls_offset_desc = dlsym(RTLD_DEFAULT, "_thread_db_link_map_l_tls_offset");
if(tls_modid_desc && tls_offset_desc)
{
if(tls_modid_desc->bit_size <= 64 && tls_offset_desc->bit_size <= 64)
{
dmn_lnx_state->tls_modid_desc = *tls_modid_desc;
dmn_lnx_state->tls_offset_desc = *tls_offset_desc;
dmn_lnx_state->is_tls_detected = 1;
}
else { Assert(0 && "invalid TLS desc"); }
}
}
}
}
////////////////////////////////
@@ -2518,7 +2553,7 @@ dmn_ctrl_run(Arena *arena, DMN_CtrlCtx *ctx, DMN_RunCtrls *ctrls)
pid_t new_pid;
if(OS_LNX_RETRY_ON_EINTR(ptrace(PTRACE_GETEVENTMSG, wait_id, 0, &new_pid)) >= 0)
{
dmn_lnx_handle_create_thread(arena, &events, wait_id, new_pid);
dmn_lnx_handle_create_thread(arena, &events, dmn_lnx_thread_from_pid(wait_id), new_pid);
}
else { Assert(0 && "failed to get new tid"); }
}break;
@@ -2742,7 +2777,33 @@ dmn_stack_base_vaddr_from_thread(DMN_Handle handle)
internal U64
dmn_tls_root_vaddr_from_thread(DMN_Handle handle)
{
return 0;
U64 tls_root_vaddr = max_U64;
DMN_AccessScope
{
DMN_LNX_Entity *thread = dmn_lnx_entity_from_handle(handle);
switch (thread->arch)
{
case Arch_Null: {} break;
case Arch_x64:
{
DMN_LNX_Entity *process = thread->parent;
REGS_RegBlockX64 *reg_block = thread->reg_block;
U64 dtv_pointer = 0;
if(dmn_lnx_read_struct(process->fd, reg_block->fsbase.u64 + 8, &dtv_pointer))
{
tls_root_vaddr = dtv_pointer;
}
} break;
case Arch_x86:
case Arch_arm32:
case Arch_arm64:
{
NotImplemented;
} break;
default: { InvalidPath; } break;
}
}
return tls_root_vaddr;
}
internal B32
+15
View File
@@ -61,6 +61,15 @@ struct DMN_LNX_UserX64
};
StaticAssert(sizeof(DMN_LNX_UserX64) == 912, g_dmn_lnx_user_x64_size_check);
////////////////////////////////
typedef struct
{
U32 bit_size;
U32 count;
U32 offset;
} DMN_LNX_DbDesc;
////////////////////////////////
//~ SDT Probes
@@ -211,6 +220,7 @@ struct DMN_LNX_Entity
B32 pass_through_signal;
U64 pass_through_signo;
DMN_LNX_ThreadState thread_state;
U64 thread_local_base;
// module
U64 base_vaddr;
@@ -292,6 +302,11 @@ struct DMN_LNX_State
U64 halt_code;
U64 halt_user_data;
B32 is_halting;
// TLS
B32 is_tls_detected;
DMN_LNX_DbDesc tls_modid_desc;
DMN_LNX_DbDesc tls_offset_desc;
};
////////////////////////////////
+105 -51
View File
@@ -465,7 +465,7 @@ dmn_w32_image_info_from_process_base_vaddr(HANDLE process, U64 base_vaddr)
U32 pe_offset = 0;
{
U64 dos_magic_off = base_vaddr;
U16 dos_magic = 0;
U16 dos_magic = 0;
dmn_w32_process_read_struct(process, dos_magic_off, &dos_magic);
if(dos_magic == PE_DOS_MAGIC)
{
@@ -475,56 +475,107 @@ dmn_w32_image_info_from_process_base_vaddr(HANDLE process, U64 base_vaddr)
}
// rjf: get COFF header
B32 got_coff_header = 0;
U64 coff_header_off = 0;
COFF_FileHeader coff_header = {0};
B32 got_coff_header = 0;
U64 coff_header_off = 0;
COFF_FileHeader coff_header = {0};
if(pe_offset > 0)
{
U64 pe_magic_off = base_vaddr + pe_offset;
U32 pe_magic = 0;
U32 pe_magic = 0;
dmn_w32_process_read_struct(process, pe_magic_off, &pe_magic);
if(pe_magic == PE_MAGIC)
{
coff_header_off = pe_magic_off + sizeof(pe_magic);
if(dmn_w32_process_read_struct(process, coff_header_off, &coff_header))
{
got_coff_header = 1;
}
got_coff_header = dmn_w32_process_read_struct(process, coff_header_off, &coff_header);
}
}
// rjf: get arch and size
DMN_W32_ImageInfo result = zero_struct;
if(got_coff_header)
{
U64 optional_size_off = 0;
Arch arch = Arch_Null;
switch(coff_header.machine)
U64 optional_off = coff_header_off + sizeof(COFF_FileHeader);
Arch arch = arch_from_coff_machine(coff_header.machine);
U64 image_size = 0;
U64 data_dir_count = 0;
U64 data_dir_off = max_U64;
U64 tls_index = max_U64;
// parse optional header
if(pe_has_plus_header(coff_header.machine))
{
case COFF_MachineType_X86:
PE_OptionalHeader32Plus opt_header = {0};
if(dmn_w32_process_read_struct(process, optional_off, &opt_header))
{
arch = Arch_x86;
optional_size_off = OffsetOf(PE_OptionalHeader32, sizeof_image);
}break;
case COFF_MachineType_X64:
{
arch = Arch_x64;
optional_size_off = OffsetOf(PE_OptionalHeader32Plus, sizeof_image);
}break;
default:
{}break;
}
if(arch != Arch_Null)
{
U64 optional_off = coff_header_off + sizeof(coff_header);
U32 size = 0;
if(dmn_w32_process_read_struct(process, optional_off+optional_size_off, &size) >= sizeof(size))
{
result.arch = arch;
result.size = size;
image_size = opt_header.sizeof_image;
data_dir_count = opt_header.data_dir_count;
data_dir_off = optional_off + sizeof(opt_header);
}
else { Assert(0 && "failed to read optional header"); }
}
else
{
PE_OptionalHeader32 opt_header = {0};
if(dmn_w32_process_read_struct(process, optional_off, &opt_header))
{
image_size = opt_header.sizeof_image;
data_dir_count = opt_header.data_dir_count;
data_dir_off = optional_off + sizeof(opt_header);
}
else { Assert(0 && "failed to read optional header"); }
}
// extract TLS index
if(PE_DataDirectoryIndex_TLS < data_dir_count)
{
U64 tls_dir_vaddr = data_dir_off + PE_DataDirectoryIndex_TLS * sizeof(PE_DataDirectory);
PE_DataDirectory tls_dir = {0};
if(dmn_w32_process_read_struct(process, tls_dir_vaddr, &tls_dir))
{
if(pe_has_plus_header(coff_header.machine))
{
if(tls_dir.virt_size == sizeof(PE_TLSHeader64))
{
PE_TLSHeader64 tls_header = {0};
if(dmn_w32_process_read_struct(process, base_vaddr + tls_dir.virt_off, &tls_header))
{
U64 tls_index64 = 0;
if(dmn_w32_process_read_struct(process, tls_header.index_address, &tls_index64))
{
tls_index = tls_index64;
}
else { Assert(0 && "failed to read TLS Index 64"); }
}
else { Assert(0 && "failed to read TLS Header 64"); }
}
}
else
{
if(tls_dir.virt_size == sizeof(PE_TLSHeader32))
{
PE_TLSHeader32 tls_header = {0};
if(dmn_w32_process_read_struct(process, base_vaddr + tls_dir.virt_off, &tls_header))
{
U32 tls_index32 = 0;
if(dmn_w32_process_read_struct(process, tls_header.index_address, &tls_index32))
{
tls_index = tls_index32;
}
else { Assert(0 && "failed to read TLS Index 32"); }
}
else { Assert(0 && "failed to read TLS Header32"); }
}
}
}
else { Assert(0 && "failed to read TLS directory"); }
}
// fill out result
result.arch = arch;
result.size = image_size;
result.tls_index = tls_index;
}
else { Assert(0 && "failed to find COFF file header"); }
return result;
}
@@ -2003,10 +2054,11 @@ dmn_ctrl_run(Arena *arena, DMN_CtrlCtx *ctx, DMN_RunCtrls *ctrls)
// rjf: create process
{
DMN_Event *e = dmn_event_list_push(arena, &events);
e->kind = DMN_EventKind_CreateProcess;
e->process = dmn_w32_handle_from_entity(process);
e->arch = image_info.arch;
e->code = evt.dwProcessId;
e->kind = DMN_EventKind_CreateProcess;
e->process = dmn_w32_handle_from_entity(process);
e->arch = image_info.arch;
e->code = evt.dwProcessId;
e->tls_model = DMN_TlsModel_WinodwsNt;
}
// rjf: create thread
@@ -2022,13 +2074,14 @@ dmn_ctrl_run(Arena *arena, DMN_CtrlCtx *ctx, DMN_RunCtrls *ctrls)
// rjf: load module
{
DMN_Event *e = dmn_event_list_push(arena, &events);
e->kind = DMN_EventKind_LoadModule;
e->process = dmn_w32_handle_from_entity(process);
e->module = dmn_w32_handle_from_entity(module);
e->arch = image_info.arch;
e->address = module_base;
e->size = image_info.size;
e->string = dmn_w32_full_path_from_module(arena, module);
e->kind = DMN_EventKind_LoadModule;
e->process = dmn_w32_handle_from_entity(process);
e->module = dmn_w32_handle_from_entity(module);
e->arch = image_info.arch;
e->address = module_base;
e->size = image_info.size;
e->string = dmn_w32_full_path_from_module(arena, module);
e->tls_index = image_info.tls_index;
}
}
}break;
@@ -2195,13 +2248,14 @@ dmn_ctrl_run(Arena *arena, DMN_CtrlCtx *ctx, DMN_RunCtrls *ctrls)
// rjf: generate event
{
DMN_Event *e = dmn_event_list_push(arena, &events);
e->kind = DMN_EventKind_LoadModule;
e->process = dmn_w32_handle_from_entity(process);
e->module = dmn_w32_handle_from_entity(module);
e->arch = module->arch;
e->address = module_base;
e->size = image_info.size;
e->string = dmn_w32_full_path_from_module(arena, module);
e->kind = DMN_EventKind_LoadModule;
e->process = dmn_w32_handle_from_entity(process);
e->module = dmn_w32_handle_from_entity(module);
e->arch = module->arch;
e->address = module_base;
e->size = image_info.size;
e->string = dmn_w32_full_path_from_module(arena, module);
e->tls_index = image_info.tls_index;
}
}break;
+1
View File
@@ -255,6 +255,7 @@ struct DMN_W32_ImageInfo
{
Arch arch;
U32 size;
U64 tls_index;
};
////////////////////////////////