diff --git a/kernel/include/kernel/Process.h b/kernel/include/kernel/Process.h
index 0ba814cb..29c1d9e4 100644
--- a/kernel/include/kernel/Process.h
+++ b/kernel/include/kernel/Process.h
@@ -182,6 +182,9 @@ namespace Kernel
 		BAN::ErrorOr<long> sys_sigpending(sigset_t* set);
 		BAN::ErrorOr<long> sys_sigprocmask(int how, const sigset_t* set, sigset_t* oset);
 
+		BAN::ErrorOr<long> sys_pthread_create(const pthread_attr_t* __restrict attr, void (*entry)(void*), void* arg);
+		BAN::ErrorOr<long> sys_pthread_exit(void* value);
+
 		BAN::ErrorOr<long> sys_tcgetpgrp(int fd);
 		BAN::ErrorOr<long> sys_tcsetpgrp(int fd, pid_t pgid);
 
@@ -290,6 +293,13 @@ namespace Kernel
 
 		BAN::Vector<Thread*> m_threads;
 
+		struct pthread_info_t
+		{
+			Thread* thread;
+			void* value;
+		};
+		BAN::Vector<pthread_info_t> m_exited_pthreads;
+
 		uint64_t m_alarm_interval_ns { 0 };
 		uint64_t m_alarm_wake_time_ns { 0 };
 
diff --git a/kernel/include/kernel/Thread.h b/kernel/include/kernel/Thread.h
index 35d05f54..a861a2bc 100644
--- a/kernel/include/kernel/Thread.h
+++ b/kernel/include/kernel/Thread.h
@@ -38,6 +38,8 @@ namespace Kernel
 		static BAN::ErrorOr<Thread*> create_userspace(Process*, PageTable&);
 		~Thread();
 
+		BAN::ErrorOr<Thread*> pthread_create(entry_t, void*);
+
 		BAN::ErrorOr<Thread*> clone(Process*, uintptr_t sp, uintptr_t ip);
 		void setup_exec();
 		void setup_process_cleanup();
diff --git a/kernel/kernel/Process.cpp b/kernel/kernel/Process.cpp
index f6ac31b0..1f6c05b6 100644
--- a/kernel/kernel/Process.cpp
+++ b/kernel/kernel/Process.cpp
@@ -185,6 +185,8 @@ namespace Kernel
 		process->m_userspace_info.envp = nullptr;
 
 		auto* thread = MUST(Thread::create_userspace(process, process->page_table()));
+		thread->setup_exec();
+
 		process->add_thread(thread);
 		process->register_to_scheduler();
 		return process;
@@ -208,6 +210,7 @@ namespace Kernel
 	Process::~Process()
 	{
 		ASSERT(m_threads.empty());
+		ASSERT(m_exited_pthreads.empty());
 		ASSERT(m_mapped_regions.empty());
 		ASSERT(!m_page_table);
 	}
@@ -238,6 +241,8 @@ namespace Kernel
 			}
 		}
 
+		m_exited_pthreads.clear();
+
 		ProcFileSystem::get().on_process_delete(*this);
 
 		m_process_lock.lock();
@@ -2079,6 +2084,43 @@ namespace Kernel
 		return 0;
 	}
 
+	BAN::ErrorOr<long> Process::sys_pthread_create(const pthread_attr_t* __restrict attr, void (*entry)(void*), void* arg)
+	{
+		if (attr != nullptr)
+		{
+			dwarnln("pthread attr not supported");
+			return BAN::Error::from_errno(ENOTSUP);
+		}
+
+		LockGuard _(m_process_lock);
+
+		auto* new_thread = TRY(Thread::current().pthread_create(entry, arg));
+		MUST(m_threads.push_back(new_thread));
+		MUST(Processor::scheduler().add_thread(new_thread));
+
+		return new_thread->tid();
+	}
+
+	BAN::ErrorOr<long> Process::sys_pthread_exit(void* value)
+	{
+		LockGuard _(m_process_lock);
+
+		// main thread cannot call pthread_exit
+		if (&Thread::current() == m_threads.front())
+			return BAN::Error::from_errno(EINVAL);
+
+		TRY(m_exited_pthreads.emplace_back(&Thread::current(), value));
+		for (auto* thread : m_threads)
+		{
+			if (thread != &Thread::current())
+				continue;
+			m_process_lock.unlock();
+			thread->on_exit();
+		}
+
+		ASSERT_NOT_REACHED();
+	}
+
 	BAN::ErrorOr<long> Process::sys_tcgetpgrp(int fd)
 	{
 		LockGuard _(m_process_lock);
diff --git a/kernel/kernel/Thread.cpp b/kernel/kernel/Thread.cpp
index 38ed8a93..c3fa38a9 100644
--- a/kernel/kernel/Thread.cpp
+++ b/kernel/kernel/Thread.cpp
@@ -117,8 +117,6 @@ namespace Kernel
 			true
 		));
 
-		thread->setup_exec();
-
 		thread_deleter.disable();
 
 		return thread;
@@ -166,6 +164,21 @@ namespace Kernel
 		}
 	}
 
+	BAN::ErrorOr<Thread*> Thread::pthread_create(entry_t entry, void* arg)
+	{
+		auto* thread = TRY(create_userspace(m_process, m_process->page_table()));
+
+		memcpy(thread->m_sse_storage, m_sse_storage, sizeof(m_sse_storage));
+
+		thread->setup_exec_impl(
+			reinterpret_cast<uintptr_t>(entry),
+			reinterpret_cast<uintptr_t>(arg),
+			0, 0, 0
+		);
+
+		return thread;
+	}
+
 	BAN::ErrorOr<Thread*> Thread::clone(Process* new_process, uintptr_t sp, uintptr_t ip)
 	{
 		ASSERT(m_is_userspace);
@@ -189,6 +202,8 @@ namespace Kernel
 		thread->m_interrupt_stack.sp = sp;
 		thread->m_interrupt_stack.ss = 0x10;
 
+		memcpy(thread->m_sse_storage, m_sse_storage, sizeof(m_sse_storage));
+
 #if ARCH(x86_64)
 		thread->m_interrupt_registers.rax = 0;
 #elif ARCH(i686)
@@ -201,23 +216,34 @@ namespace Kernel
 	}
 
 	void Thread::setup_exec()
+	{
+		const auto& userspace_info = process().userspace_info();
+		ASSERT(userspace_info.entry);
+
+		setup_exec_impl(
+			userspace_info.entry,
+			userspace_info.argc,
+			reinterpret_cast<uintptr_t>(userspace_info.argv),
+			reinterpret_cast<uintptr_t>(userspace_info.envp),
+			userspace_info.file_fd
+		);
+	}
+
+	void Thread::setup_exec_impl(uintptr_t entry, uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3)
 	{
 		ASSERT(is_userspace());
 		m_state = State::NotStarted;
 
 		// Signal mask is inherited
 
-		const auto& userspace_info = process().userspace_info();
-		ASSERT(userspace_info.entry);
-
 		// Initialize stack for returning
 		PageTable::with_fast_page(process().page_table().physical_address_of(kernel_stack_top() - PAGE_SIZE), [&] {
 			uintptr_t sp = PageTable::fast_page() + PAGE_SIZE;
-			write_to_stack(sp, userspace_info.entry);
-			write_to_stack(sp, userspace_info.file_fd);
-			write_to_stack(sp, userspace_info.envp);
-			write_to_stack(sp, userspace_info.argv);
-			write_to_stack(sp, userspace_info.argc);
+			write_to_stack(sp, entry);
+			write_to_stack(sp, arg3);
+			write_to_stack(sp, arg2);
+			write_to_stack(sp, arg1);
+			write_to_stack(sp, arg0);
 		});
 
 		m_interrupt_stack.ip = reinterpret_cast<vaddr_t>(start_userspace_thread);
diff --git a/userspace/libraries/LibC/CMakeLists.txt b/userspace/libraries/LibC/CMakeLists.txt
index e4afbc77..abb54937 100644
--- a/userspace/libraries/LibC/CMakeLists.txt
+++ b/userspace/libraries/LibC/CMakeLists.txt
@@ -17,6 +17,7 @@ set(LIBC_SOURCES
 	netdb.cpp
 	poll.cpp
 	printf_impl.cpp
+	pthread.cpp
 	pwd.cpp
 	scanf_impl.cpp
 	setjmp.cpp
diff --git a/userspace/libraries/LibC/include/sys/syscall.h b/userspace/libraries/LibC/include/sys/syscall.h
index e8a2bb2e..9e0c0529 100644
--- a/userspace/libraries/LibC/include/sys/syscall.h
+++ b/userspace/libraries/LibC/include/sys/syscall.h
@@ -90,6 +90,8 @@ __BEGIN_DECLS
 	O(SYS_FSYNC,			fsync)			\
 	O(SYS_SYMLINKAT,		symlinkat)		\
 	O(SYS_HARDLINKAT,		hardlinkat)		\
+	O(SYS_PTHREAD_CREATE,	pthread_create)	\
+	O(SYS_PTHREAD_EXIT,		pthread_exit)	\
 
 enum Syscall
 {
diff --git a/userspace/libraries/LibC/pthread.cpp b/userspace/libraries/LibC/pthread.cpp
new file mode 100644
index 00000000..80399312
--- /dev/null
+++ b/userspace/libraries/LibC/pthread.cpp
@@ -0,0 +1,49 @@
+#include <BAN/Assert.h>
+
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+struct pthread_trampoline_info_t
+{
+	void* (*start_routine)(void*);
+	void* arg;
+};
+
+static void pthread_trampoline(void* arg)
+{
+	pthread_trampoline_info_t info;
+	memcpy(&info, arg, sizeof(pthread_trampoline_info_t));
+	free(arg);
+
+	pthread_exit(info.start_routine(info.arg));
+	ASSERT_NOT_REACHED();
+}
+
+int pthread_create(pthread_t* __restrict thread, const pthread_attr_t* __restrict attr, void* (*start_routine)(void*), void* __restrict arg)
+{
+	auto* info = static_cast<pthread_trampoline_info_t*>(malloc(sizeof(pthread_trampoline_info_t)));
+	if (info == nullptr)
+		return -1;
+	info->start_routine = start_routine;
+	info->arg = arg;
+
+	const auto ret = syscall(SYS_PTHREAD_CREATE, attr, pthread_trampoline, info);
+	if (ret == -1)
+	{
+		free(info);
+		return -1;
+	}
+
+	if (thread)
+		*thread = ret;
+	return 0;
+}
+
+void pthread_exit(void* value_ptr)
+{
+	syscall(SYS_PTHREAD_EXIT, value_ptr);
+	ASSERT_NOT_REACHED();
+}
diff --git a/userspace/tests/test-pthread/CMakeLists.txt b/userspace/tests/test-pthread/CMakeLists.txt
new file mode 100644
index 00000000..59880e4d
--- /dev/null
+++ b/userspace/tests/test-pthread/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(SOURCES
+	main.cpp
+)
+
+add_executable(test-pthread ${SOURCES})
+banan_link_library(test-pthread libc)
+
+install(TARGETS test-pthread OPTIONAL)
diff --git a/userspace/tests/test-pthread/main.cpp b/userspace/tests/test-pthread/main.cpp
new file mode 100644
index 00000000..53562070
--- /dev/null
+++ b/userspace/tests/test-pthread/main.cpp
@@ -0,0 +1,28 @@
+#include <stdio.h>
+#include <pthread.h>
+#include <unistd.h>
+
+void* thread_func(void*)
+{
+	printf("hello from thread\n");
+	return nullptr;
+}
+
+int main(int argc, char** argv)
+{
+	pthread_t tid;
+
+	printf("creating thread\n");
+
+	if (pthread_create(&tid, nullptr, &thread_func, nullptr) == -1)
+	{
+		perror("pthread_create");
+		return 1;
+	}
+
+	sleep(1);
+
+	printf("exiting\n");
+
+	return 0;
+}