Index: uspace/lib/c/generic/libc.c
===================================================================
--- uspace/lib/c/generic/libc.c	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/c/generic/libc.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -81,4 +81,5 @@
 		main_fibril.tcb = tls_make_initial(__progsymbols.elfstart);
 	}
+	main_fibril.is_freeable = false;
 
 	assert(main_fibril.tcb);
@@ -172,4 +173,6 @@
 void __libc_exit(int status)
 {
+	fibril_run_exit_hooks(&main_fibril);
+
 	/*
 	 * GCC extension __attribute__((destructor)),
Index: uspace/lib/c/generic/private/fibril.h
===================================================================
--- uspace/lib/c/generic/private/fibril.h	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/c/generic/private/fibril.h	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -70,5 +70,12 @@
 	fibril_owner_info_t *waits_for;
 	fibril_event_t *sleep_event;
+
+	list_t exit_hooks;
 };
+
+typedef struct {
+	link_t link;
+	void (*func)(void);
+} fibril_hook_t;
 
 extern fibril_t *fibril_alloc(void);
@@ -86,4 +93,6 @@
 extern errno_t fibril_ipc_wait(ipc_call_t *, const struct timespec *);
 extern void fibril_ipc_poke(void);
+
+extern void fibril_run_exit_hooks(fibril_t *);
 
 /**
Index: uspace/lib/c/generic/rtld/symbol.c
===================================================================
--- uspace/lib/c/generic/rtld/symbol.c	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/c/generic/rtld/symbol.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -65,4 +65,9 @@
 static elf_symbol_t *def_find_in_module(const char *name, module_t *m)
 {
+	if (m->dyn.hash == NULL) {
+		/* No hash table */
+		return NULL;
+	}
+
 	elf_symbol_t *sym_table;
 	elf_symbol_t *s, *sym;
Index: uspace/lib/c/generic/thread/fibril.c
===================================================================
--- uspace/lib/c/generic/thread/fibril.c	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/c/generic/thread/fibril.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -185,4 +185,5 @@
 void fibril_setup(fibril_t *f)
 {
+	list_initialize(&f->exit_hooks);
 	futex_lock(&fibril_futex);
 	list_append(&f->all_link, &fibril_list);
@@ -845,4 +846,14 @@
 }
 
+void fibril_run_exit_hooks(fibril_t *f)
+{
+	list_foreach_safe(f->exit_hooks, cur, _next) {
+		fibril_hook_t *hook = list_get_instance(cur, fibril_hook_t, link);
+		list_remove(cur);
+		hook->func();
+		free(hook);
+	}
+}
+
 /**
  * Exit a fibril. Never returns.
@@ -854,4 +865,6 @@
 	// TODO: implement fibril_join() and remember retval
 	(void) retval;
+
+	fibril_run_exit_hooks(fibril_self());
 
 	fibril_t *f = _ready_list_pop_nonblocking(false);
@@ -918,4 +931,17 @@
 }
 
+errno_t fibril_add_exit_hook(void (*hook)(void))
+{
+	fibril_hook_t *h = malloc(sizeof(fibril_hook_t));
+	if (!h)
+		return ENOMEM;
+
+	DPRINTF("adding exit hook: function %p (fibril_hook_t structure at %p)\n", hook, h);
+
+	h->func = hook;
+	list_append(&h->link, &fibril_self()->exit_hooks);
+	return EOK;
+}
+
 errno_t fibril_ipc_wait(ipc_call_t *call, const struct timespec *expires)
 {
Index: uspace/lib/c/include/fibril.h
===================================================================
--- uspace/lib/c/include/fibril.h	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/c/include/fibril.h	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -74,4 +74,7 @@
 extern __noreturn void fibril_exit(long);
 
+/** Add a function to be called after fibril exits, just before it is destroyed */
+extern errno_t fibril_add_exit_hook(void (*)(void));
+
 __HELENOS_DECLS_END;
 
Index: uspace/lib/c/meson.build
===================================================================
--- uspace/lib/c/meson.build	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/c/meson.build	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -177,4 +177,5 @@
 	'test/double_to_str.c',
 	'test/fibril/timer.c',
+	'test/fibril/exit_hook.c',
 	'test/getopt.c',
 	'test/gsort.c',
Index: uspace/lib/c/test/fibril/exit_hook.c
===================================================================
--- uspace/lib/c/test/fibril/exit_hook.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
+++ uspace/lib/c/test/fibril/exit_hook.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2025 Matej Volf
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <fibril.h>
+#include <pcut/pcut.h>
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(fibril_exit_hook);
+
+static int value;
+
+static void exit_hook(void)
+{
+	value = 5;
+}
+
+static errno_t fibril_basic(void *_arg)
+{
+	fibril_add_exit_hook(exit_hook);
+	return EOK;
+}
+
+PCUT_TEST(exit_hook_basic)
+{
+	value = 0;
+	fid_t other = fibril_create(fibril_basic, NULL);
+	fibril_start(other);
+
+	fibril_yield();
+
+	PCUT_ASSERT_INT_EQUALS(5, value);
+}
+
+static errno_t fibril_explicit_exit(void *_arg)
+{
+	fibril_add_exit_hook(exit_hook);
+	fibril_exit(ETIMEOUT);
+}
+
+PCUT_TEST(exit_hook_explicit_exit)
+{
+	value = 0;
+	fid_t other = fibril_create(fibril_explicit_exit, NULL);
+	fibril_start(other);
+
+	fibril_yield();
+
+	PCUT_ASSERT_INT_EQUALS(5, value);
+}
+
+#ifdef FIBRIL_KILL_HAS_BEEN_IMPLEMENTED
+static errno_t fibril_to_be_killed(void *_arg)
+{
+	fibril_add_exit_hook(exit_hook);
+
+	while (true)
+		firbil_yield();
+
+	assert(0 && "unreachable");
+}
+
+PCUT_TEST(exit_hook_kill)
+{
+	value = 0;
+	fid_t other = fibril_create(fibril_to_be_killed, NULL);
+	fibril_start(other);
+
+	fibril_yield();
+
+	fibril_kill(other); // anything like this doesn't exist yet
+
+	PCUT_ASSERT_INT_EQUALS(5, value);
+}
+#endif // FIBRIL_KILL_HAS_BEEN_IMPLEMENTED
+
+PCUT_EXPORT(fibril_exit_hook);
Index: uspace/lib/c/test/main.c
===================================================================
--- uspace/lib/c/test/main.c	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/c/test/main.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -38,4 +38,5 @@
 PCUT_IMPORT(double_to_str);
 PCUT_IMPORT(fibril_timer);
+PCUT_IMPORT(fibril_exit_hook);
 PCUT_IMPORT(getopt);
 PCUT_IMPORT(gsort);
Index: uspace/lib/posix/meson.build
===================================================================
--- uspace/lib/posix/meson.build	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/posix/meson.build	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -61,4 +61,5 @@
 	'test/stdlib.c',
 	'test/unistd.c',
+	'test/pthread/keys.c',
 )
 
Index: uspace/lib/posix/src/pthread/keys.c
===================================================================
--- uspace/lib/posix/src/pthread/keys.c	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/posix/src/pthread/keys.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -36,28 +36,100 @@
 #include <pthread.h>
 #include <errno.h>
+#include <fibril.h>
+#include <stdatomic.h>
 #include "../internal/common.h"
+
+#define DPRINTF(format, ...) ((void) 0)
+
+static fibril_local bool fibril_initialized = false;
+static atomic_ushort next_key = 1; // skip the key 'zero'
+
+/*
+ * For now, we just support maximum of 100 keys. This can be improved
+ * in the future by implementing a dynamically growing array with
+ * reallocations, but that will require more synchronization.
+ */
+#define PTHREAD_KEYS_MAX 100
+static void (*destructors[PTHREAD_KEYS_MAX])(void *);
+
+static fibril_local void *key_data[PTHREAD_KEYS_MAX];
 
 void *pthread_getspecific(pthread_key_t key)
 {
-	not_implemented();
-	return NULL;
+	// initialization is done in setspecific -> if not initialized, nothing was set yet
+	if (!fibril_initialized)
+		return NULL;
+
+	assert(key < PTHREAD_KEYS_MAX);
+	assert(key < next_key);
+	assert(key > 0);
+
+	return key_data[key];
+}
+
+static void pthread_key_on_fibril_exit(void)
+{
+	if (!fibril_initialized)
+		return;
+
+	for (unsigned i = 0; i < PTHREAD_KEYS_MAX; i++) {
+		/*
+		 * Note that this doesn't cause a race with pthread_key_create:
+		 * if key `i` has not been assigned yet (it could be just being
+		 * created), key_data[i] has never been assigned, therefore it
+		 * is NULL, and the destructor is not checked at all.
+		 */
+		if (key_data[i] != NULL && destructors[i] != NULL)
+			destructors[i](key_data[i]);
+	}
 }
 
 int pthread_setspecific(pthread_key_t key, const void *data)
 {
-	not_implemented();
-	return ENOTSUP;
+	if (!fibril_initialized) {
+		DPRINTF("initializing pthread keys\n");
+		errno_t res = fibril_add_exit_hook(pthread_key_on_fibril_exit);
+		if (res != EOK)
+			return res;
+
+		for (unsigned i = 0; i < PTHREAD_KEYS_MAX; i++) {
+			key_data[i] = NULL;
+		}
+		fibril_initialized = true;
+	}
+	assert(key < PTHREAD_KEYS_MAX);
+	assert(key < next_key);
+	assert(key > 0);
+
+	key_data[key] = (void *) data;
+	return EOK;
 }
 
 int pthread_key_delete(pthread_key_t key)
 {
-	not_implemented();
-	return ENOTSUP;
+	/*
+	 * FIXME: this can cause a data race if another fibrill concurrently
+	 * runs on_fibril_exit. The obvious solution is to add a rwlock on
+	 * the destructors array, which will be needed anyway if we want to
+	 * support unlimited number of keys.
+	 */
+	destructors[key] = NULL;
+	key_data[key] = NULL;
+
+	// TODO: the key could also be reused
+	return EOK;
 }
 
 int pthread_key_create(pthread_key_t *key, void (*destructor)(void *))
 {
-	not_implemented();
-	return ENOTSUP;
+	unsigned short k = atomic_fetch_add(&next_key, 1);
+	if (k >= PTHREAD_KEYS_MAX) {
+		atomic_store(&next_key, PTHREAD_KEYS_MAX + 1);
+		return ELIMIT;
+	}
+
+	destructors[k] = destructor;
+	*key = k;
+	return EOK;
 }
 
Index: uspace/lib/posix/test/main.c
===================================================================
--- uspace/lib/posix/test/main.c	(revision 797ab957fc37b27462c54f0c3a7520af8a33e233)
+++ uspace/lib/posix/test/main.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -34,4 +34,5 @@
 PCUT_IMPORT(stdlib);
 PCUT_IMPORT(unistd);
+PCUT_IMPORT(pthread_keys);
 
 PCUT_MAIN();
Index: uspace/lib/posix/test/pthread/keys.c
===================================================================
--- uspace/lib/posix/test/pthread/keys.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
+++ uspace/lib/posix/test/pthread/keys.c	(revision 88cbc669c2905a475a2b1bb99d9643480ecb42b0)
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2025 Matej Volf
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <fibril.h>
+#include <posix/pthread.h>
+#include <pcut/pcut.h>
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(pthread_keys);
+
+pthread_key_t key;
+int destructors_executed;
+
+static void destructor(void *_data)
+{
+	destructors_executed++;
+}
+
+static errno_t simple_fibril(void *_arg)
+{
+	PCUT_ASSERT_INT_EQUALS(0, pthread_setspecific(key, (void *) 0x0d9e));
+	PCUT_ASSERT_PTR_EQUALS((void *) 0x0d9e, pthread_getspecific(key));
+
+	for (int i = 0; i < 10; i++) {
+		fibril_yield();
+	}
+
+	return EOK;
+}
+
+PCUT_TEST(pthread_keys_basic)
+{
+	destructors_executed = 0;
+	PCUT_ASSERT_INT_EQUALS(0, pthread_key_create(&key, destructor));
+	PCUT_ASSERT_PTR_EQUALS(NULL, pthread_getspecific(key));
+
+	PCUT_ASSERT_INT_EQUALS(0, pthread_setspecific(key, (void *) 0x42));
+	PCUT_ASSERT_PTR_EQUALS((void *) 0x42, pthread_getspecific(key));
+
+	fid_t other = fibril_create(simple_fibril, NULL);
+	fibril_start(other);
+
+	for (int i = 0; i < 5; i++) {
+		fibril_yield();
+	}
+
+	PCUT_ASSERT_INT_EQUALS(0, destructors_executed);
+	PCUT_ASSERT_PTR_EQUALS((void *) 0x42, pthread_getspecific(key));
+
+	for (int i = 0; i < 10; i++) {
+		fibril_yield();
+	}
+
+	PCUT_ASSERT_INT_EQUALS(1, destructors_executed);
+	PCUT_ASSERT_PTR_EQUALS((void *) 0x42, pthread_getspecific(key));
+}
+
+PCUT_EXPORT(pthread_keys);
