[libc-commits] [libc] [libc] Apply scheduler attributes on thread creation (PR #225674)

Pavel Labath via libc-commits libc-commits at lists.llvm.org
Wed Sep 23 04:59:24 PDT 2026


https://github.com/labath updated https://github.com/llvm/llvm-project/pull/225674

>From 59a77042908b336651de6b24de15a95da32296b2 Mon Sep 17 00:00:00 2001
From: Pavel Labath <pavel at labath.sk>
Date: Tue, 22 Sep 2026 12:04:32 +0000
Subject: [PATCH] [libc] Apply scheduler attributes on thread creation

POSIX specifies that pthread_create should apply the scheduling policy
and parameters given in the attributes object (if PTHREAD_EXPLICIT_SCHED
is set). Linux's clone syscall doesn't support setting scheduling
parameters directly, so we have to apply them from userspace via
sched_setscheduler after the thread is created.

To make sure the thread doesn't start running user code with the wrong
scheduling parameters (and to allow returning an error if
sched_setscheduler fails without leaving a half-running thread), I've
added a startup handshake using the clear_tid futex on the child stack:
- the child waits on the futex if explicit scheduling was requested
- the parent calls setschedparam, and then signals the child to either
  continue (running) or abort (exit)
- if the call fails, the child exits immediately, the parent reaps it
  via Thread::wait(), and returns the error code to the caller

Assisted-by: Gemini
---
 .../__support/threads/linux/CMakeLists.txt    |   1 +
 libc/src/__support/threads/linux/thread.cpp   |  60 +++++-
 libc/src/__support/threads/thread.h           |  10 +-
 libc/src/pthread/pthread_create.cpp           |  17 +-
 .../integration/src/pthread/CMakeLists.txt    |   8 +
 .../src/pthread/pthread_create_test.cpp       | 195 ++++++++++++++++++
 6 files changed, 276 insertions(+), 15 deletions(-)

diff --git a/libc/src/__support/threads/linux/CMakeLists.txt b/libc/src/__support/threads/linux/CMakeLists.txt
index d82b0c8e979187..ded24b9b25417e 100644
--- a/libc/src/__support/threads/linux/CMakeLists.txt
+++ b/libc/src/__support/threads/linux/CMakeLists.txt
@@ -70,6 +70,7 @@ add_object_library(
     libc.hdr.sys_mman_macros
     libc.src.errno.errno
     libc.src.__support.CPP.atomic
+    libc.src.__support.CPP.optional
     libc.src.__support.CPP.stringstream
     libc.src.__support.CPP.string_view
     libc.src.__support.common
diff --git a/libc/src/__support/threads/linux/thread.cpp b/libc/src/__support/threads/linux/thread.cpp
index df294e2e4b4fbf..df2d6cf6be8bfa 100644
--- a/libc/src/__support/threads/linux/thread.cpp
+++ b/libc/src/__support/threads/linux/thread.cpp
@@ -9,6 +9,7 @@
 #include "src/__support/threads/thread.h"
 #include "config/app.h"
 #include "src/__support/CPP/atomic.h"
+#include "src/__support/CPP/optional.h"
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/CPP/stringstream.h"
 #include "src/__support/OSUtil/linux/syscall_wrappers/clone.h"
@@ -145,9 +146,29 @@ cleanup_thread_resources(ThreadAttributes *attrib) {
     free_stack(attrib->stack, attrib->stacksize, attrib->guardsize);
 }
 
+// Enum values used for controlling thread startup.
+enum class ClearTidState : FutexWordType {
+  EXITED = 0, // Set by the kernel when the thread exits.
+  WAITING,    // Waiting for additional initialization.
+  RUNNING,    // Initialization complete or not needed.
+  ABORT,      // Initialization failed.
+};
+
 static int start_thread(void *arg) {
   auto *start_args = reinterpret_cast<StartArgs *>(arg);
   auto *attrib = start_args->thread_attrib;
+  auto *clear_tid = static_cast<Futex *>(attrib->platform_data);
+
+  // If explicit scheduling parameters were requested, wait for the parent to
+  // apply them via setschedparam. If setschedparam fails, the parent signals
+  // ABORT, and the child exits immediately without running user code.
+  clear_tid->wait(static_cast<FutexWordType>(ClearTidState::WAITING),
+                  cpp::nullopt, /*is_shared=*/false);
+  if (clear_tid->load(cpp::MemoryOrder::ACQUIRE) ==
+      static_cast<FutexWordType>(ClearTidState::ABORT)) {
+    LIBC_NAMESPACE::syscall_impl<long>(SYS_exit, 0);
+    __builtin_unreachable();
+  }
 
   if (attrib->style == ThreadStyle::POSIX) {
     ThreadReturnValue retval = start_args->runner.posix_runner(start_args->arg);
@@ -160,7 +181,8 @@ static int start_thread(void *arg) {
 }
 
 int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
-                size_t stacksize, size_t guardsize, bool detached) {
+                size_t stacksize, size_t guardsize, bool detached,
+                cpp::optional<SchedParameters> sched_params) {
   bool owned_stack = false;
   if (stack == nullptr) {
     // TODO: Should we return EINVAL here? Should we have a generic concept of a
@@ -249,17 +271,18 @@ int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
   attrib->tls_size = tls.size;
   attrib->joiner = nullptr;
 
-  start_args->thread_attrib = attrib;
-  start_args->runner = runner;
-  start_args->arg = arg;
-
-  auto clear_tid = reinterpret_cast<Futex *>(
+  auto *clear_tid = reinterpret_cast<Futex *>(
       adjusted_stack + sizeof(StartArgs) + sizeof(ThreadAttributes));
-  clear_tid->set(1);
+  clear_tid->set(static_cast<FutexWordType>(
+      sched_params ? ClearTidState::WAITING : ClearTidState::RUNNING));
   attrib->platform_data = clear_tid;
 
   get_tcb(tls.tp)->attrib = attrib;
 
+  start_args->thread_attrib = attrib;
+  start_args->runner = runner;
+  start_args->arg = arg;
+
   auto clone_result = linux_syscalls::clone(
       start_thread, reinterpret_cast<void *>(adjusted_stack),
       CLONE_SYSCALL_FLAGS, start_args, &attrib->tid,
@@ -268,10 +291,29 @@ int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
 
   if (!clone_result.has_value()) {
     cleanup_thread_resources(attrib);
+    attrib = nullptr;
     return clone_result.error();
   }
 
-  return 0;
+  if (!sched_params)
+    return 0;
+
+  // Linux clone does not accept scheduling parameters directly, so we must
+  // apply them after thread creation. If setschedparam fails, signal the child
+  // to abort, wait for it to exit, and clean up its resources.
+  int sched_result = setschedparam(*sched_params);
+  ClearTidState state =
+      sched_result != 0 ? ClearTidState::ABORT : ClearTidState::RUNNING;
+  clear_tid->store(static_cast<FutexWordType>(state),
+                   cpp::MemoryOrder::RELEASE);
+  clear_tid->notify_one(/*is_shared=*/false);
+
+  if (sched_result != 0) {
+    wait();
+    cleanup_thread_resources(attrib);
+    attrib = nullptr;
+  }
+  return sched_result;
 }
 
 int Thread::join(ThreadReturnValue &retval) {
@@ -329,7 +371,7 @@ void Thread::wait() {
   // The kernel should set the value at the clear tid address to zero.
   // If not, it is a spurious wake and we should continue to wait on
   // the futex.
-  auto *clear_tid = reinterpret_cast<Futex *>(attrib->platform_data);
+  auto *clear_tid = static_cast<Futex *>(attrib->platform_data);
   // We cannot do a FUTEX_WAIT_PRIVATE here as the kernel does a
   // FUTEX_WAKE and not a FUTEX_WAKE_PRIVATE.
   FutexWordType clear_tid_value;
diff --git a/libc/src/__support/threads/thread.h b/libc/src/__support/threads/thread.h
index 9971dc5f193a07..3f71e101c3714f 100644
--- a/libc/src/__support/threads/thread.h
+++ b/libc/src/__support/threads/thread.h
@@ -71,11 +71,12 @@ struct Thread {
   int run(ThreadRunnerPosix *func, void *arg, void *stack = nullptr,
           size_t stacksize = DEFAULT_STACKSIZE,
           size_t guardsize = DEFAULT_GUARDSIZE,
-          bool detached = DEFAULT_DETACHED) {
+          bool detached = DEFAULT_DETACHED,
+          cpp::optional<SchedParameters> sched_params = cpp::nullopt) {
     ThreadRunner runner;
     runner.posix_runner = func;
     return run(ThreadStyle::POSIX, runner, arg, stack, stacksize, guardsize,
-               detached);
+               detached, sched_params);
   }
 
   int run(ThreadRunnerStdc *func, void *arg, void *stack = nullptr,
@@ -85,7 +86,7 @@ struct Thread {
     ThreadRunner runner;
     runner.stdc_runner = func;
     return run(ThreadStyle::STDC, runner, arg, stack, stacksize, guardsize,
-               detached);
+               detached, cpp::nullopt);
   }
 
   int join(int *val) {
@@ -112,7 +113,8 @@ struct Thread {
 
   // Return 0 on success or an error value on failure.
   int run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
-          size_t stacksize, size_t guardsize, bool detached);
+          size_t stacksize, size_t guardsize, bool detached,
+          cpp::optional<SchedParameters> sched_params = cpp::nullopt);
 
   // Return 0 on success or an error value on failure.
   int join(ThreadReturnValue &retval);
diff --git a/libc/src/pthread/pthread_create.cpp b/libc/src/pthread/pthread_create.cpp
index 247a80c8c2c4a1..4308134d91aff0 100644
--- a/libc/src/pthread/pthread_create.cpp
+++ b/libc/src/pthread/pthread_create.cpp
@@ -39,6 +39,18 @@ LLVM_LIBC_FUNCTION(int, pthread_create,
   size_t guardsize = attr->__guardsize;
   int detachstate = attr->__detachstate;
 
+  cpp::optional<SchedParameters> sched_params;
+  switch (attr->__inheritsched) {
+  case PTHREAD_INHERIT_SCHED:
+    break;
+  case PTHREAD_EXPLICIT_SCHED:
+    sched_params = cpp::optional<SchedParameters>(
+        SchedParameters{attr->__schedpolicy, attr->__schedparam});
+    break;
+  default:
+    return EINVAL;
+  }
+
   if (stacksize && stacksize < PTHREAD_STACK_MIN)
     return EINVAL;
 
@@ -53,8 +65,9 @@ LLVM_LIBC_FUNCTION(int, pthread_create,
   // universal, not sure a pthread requirement).
 
   auto *thread = reinterpret_cast<LIBC_NAMESPACE::Thread *>(th);
-  int result = thread->run(func, arg, stack, stacksize, guardsize,
-                           detachstate == PTHREAD_CREATE_DETACHED);
+  int result =
+      thread->run(func, arg, stack, stacksize, guardsize,
+                  detachstate == PTHREAD_CREATE_DETACHED, sched_params);
   if (result != 0 && result != EPERM && result != EINVAL)
     return EAGAIN;
   return result;
diff --git a/libc/test/integration/src/pthread/CMakeLists.txt b/libc/test/integration/src/pthread/CMakeLists.txt
index 66721f8a673584..62c4dc907d3076 100644
--- a/libc/test/integration/src/pthread/CMakeLists.txt
+++ b/libc/test/integration/src/pthread/CMakeLists.txt
@@ -294,7 +294,10 @@ add_integration_test(
   SRCS
     pthread_create_test.cpp
   DEPENDS
+    libc.hdr.pthread_macros
+    libc.hdr.sched_macros
     libc.hdr.sys_mman_macros
+    libc.hdr.types.struct_sched_param
     libc.include.pthread
     libc.src.errno.errno
     libc.src.pthread.pthread_create
@@ -305,10 +308,14 @@ add_integration_test(
     libc.src.pthread.pthread_attr_getstacksize
     libc.src.pthread.pthread_attr_setdetachstate
     libc.src.pthread.pthread_attr_setguardsize
+    libc.src.pthread.pthread_attr_setinheritsched
+    libc.src.pthread.pthread_attr_setschedparam
+    libc.src.pthread.pthread_attr_setschedpolicy
     libc.src.pthread.pthread_attr_setstack
     libc.src.pthread.pthread_attr_setstacksize
     libc.src.pthread.pthread_attr_init
     libc.src.pthread.pthread_attr_destroy
+    libc.src.pthread.pthread_getschedparam
     libc.src.pthread.pthread_getunique_np
     libc.src.pthread.pthread_self
     libc.src.sys.mman.mmap
@@ -318,6 +325,7 @@ add_integration_test(
     libc.src.__support.CPP.atomic
     libc.src.__support.CPP.array
     libc.src.__support.CPP.new
+    libc.src.__support.CPP.optional
 )
 
 add_integration_test(
diff --git a/libc/test/integration/src/pthread/pthread_create_test.cpp b/libc/test/integration/src/pthread/pthread_create_test.cpp
index dd8d3ef1c8e353..5b43c74314eb7a 100644
--- a/libc/test/integration/src/pthread/pthread_create_test.cpp
+++ b/libc/test/integration/src/pthread/pthread_create_test.cpp
@@ -6,7 +6,10 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "hdr/pthread_macros.h"
+#include "hdr/sched_macros.h"
 #include "hdr/sys_mman_macros.h"
+#include "hdr/types/struct_sched_param.h"
 #include "src/pthread/pthread_attr_destroy.h"
 #include "src/pthread/pthread_attr_getdetachstate.h"
 #include "src/pthread/pthread_attr_getguardsize.h"
@@ -15,9 +18,13 @@
 #include "src/pthread/pthread_attr_init.h"
 #include "src/pthread/pthread_attr_setdetachstate.h"
 #include "src/pthread/pthread_attr_setguardsize.h"
+#include "src/pthread/pthread_attr_setinheritsched.h"
+#include "src/pthread/pthread_attr_setschedparam.h"
+#include "src/pthread/pthread_attr_setschedpolicy.h"
 #include "src/pthread/pthread_attr_setstack.h"
 #include "src/pthread/pthread_attr_setstacksize.h"
 #include "src/pthread/pthread_create.h"
+#include "src/pthread/pthread_getschedparam.h"
 #include "src/pthread/pthread_getunique_np.h"
 #include "src/pthread/pthread_join.h"
 #include "src/pthread/pthread_self.h"
@@ -334,11 +341,199 @@ static void run_failure_tests() {
   ASSERT_ERRNO_SUCCESS();
   attr.__detachstate = -1;
   create_and_check_failure_thread(&attr);
+
+  // Inheritsched is unknown.
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+  ASSERT_ERRNO_SUCCESS();
+  attr.__inheritsched = -1;
+  create_and_check_failure_thread(&attr);
+
+  // Schedpolicy is invalid when explicit sched is requested.
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+                &attr, PTHREAD_EXPLICIT_SCHED),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+  attr.__schedpolicy = -1;
+  create_and_check_failure_thread(&attr);
+
+  // Sched priority is invalid for policy.
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+                &attr, PTHREAD_EXPLICIT_SCHED),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, SCHED_OTHER), 0);
+  ASSERT_ERRNO_SUCCESS();
+  sched_param bad_param;
+  bad_param.sched_priority = 1;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedparam(&attr, &bad_param), 0);
+  ASSERT_ERRNO_SUCCESS();
+  create_and_check_failure_thread(&attr);
+}
+
+struct SchedThreadArgs {
+  LIBC_NAMESPACE::cpp::Atomic<bool> executed = false;
+  int policy = 0;
+  int priority = 0;
+};
+
+static void *sched_runner(void *arg) {
+  auto *args = reinterpret_cast<SchedThreadArgs *>(arg);
+  sched_param param;
+  int policy;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_getschedparam(
+                LIBC_NAMESPACE::pthread_self(), &policy, &param),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+  args->policy = policy;
+  args->priority = param.sched_priority;
+  args->executed.store(true);
+  return nullptr;
+}
+
+static void test_sched_inherit() {
+  pthread_t self = LIBC_NAMESPACE::pthread_self();
+  int parent_policy = 0;
+  sched_param parent_param;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_getschedparam(self, &parent_policy,
+                                                  &parent_param),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+
+  // 1. With default attr (nullptr)
+  {
+    SchedThreadArgs args;
+    pthread_t tid;
+    ASSERT_EQ(
+        LIBC_NAMESPACE::pthread_create(&tid, nullptr, sched_runner, &args), 0);
+    ASSERT_ERRNO_SUCCESS();
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+    ASSERT_ERRNO_SUCCESS();
+    ASSERT_TRUE(args.executed.load());
+    ASSERT_EQ(args.policy, parent_policy);
+    ASSERT_EQ(args.priority, parent_param.sched_priority);
+  }
+
+  // 2. Explicit PTHREAD_INHERIT_SCHED in attr, while setting schedpolicy to
+  // SCHED_BATCH
+  {
+    pthread_attr_t attr;
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+    ASSERT_ERRNO_SUCCESS();
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+                  &attr, PTHREAD_INHERIT_SCHED),
+              0);
+    ASSERT_ERRNO_SUCCESS();
+    // Even if schedpolicy is modified in attr, PTHREAD_INHERIT_SCHED means it
+    // must be ignored.
+    int dummy_policy =
+        (parent_policy == SCHED_OTHER) ? SCHED_BATCH : SCHED_OTHER;
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, dummy_policy),
+              0);
+    ASSERT_ERRNO_SUCCESS();
+
+    SchedThreadArgs args;
+    pthread_t tid;
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&tid, &attr, sched_runner, &args),
+              0);
+    ASSERT_ERRNO_SUCCESS();
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+    ASSERT_ERRNO_SUCCESS();
+    ASSERT_TRUE(args.executed.load());
+    ASSERT_EQ(args.policy, parent_policy);
+    ASSERT_EQ(args.priority, parent_param.sched_priority);
+
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_destroy(&attr), 0);
+    ASSERT_ERRNO_SUCCESS();
+  }
+}
+
+static void verify_sched_policy(pthread_attr_t &attr, int expected_policy,
+                                int expected_priority) {
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, expected_policy),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+  sched_param param;
+  param.sched_priority = expected_priority;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedparam(&attr, &param), 0);
+  ASSERT_ERRNO_SUCCESS();
+
+  SchedThreadArgs args;
+  pthread_t tid;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&tid, &attr, sched_runner, &args),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_TRUE(args.executed.load());
+  ASSERT_EQ(args.policy, expected_policy);
+  ASSERT_EQ(args.priority, expected_priority);
+}
+
+static void test_sched_explicit_success() {
+  pthread_attr_t attr;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+                &attr, PTHREAD_EXPLICIT_SCHED),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+
+  verify_sched_policy(attr, SCHED_OTHER, 0);
+  verify_sched_policy(attr, SCHED_BATCH, 0);
+
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_destroy(&attr), 0);
+  ASSERT_ERRNO_SUCCESS();
+}
+
+static void test_sched_realtime_permission() {
+  pthread_attr_t attr;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+                &attr, PTHREAD_EXPLICIT_SCHED),
+            0);
+  ASSERT_ERRNO_SUCCESS();
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, SCHED_FIFO), 0);
+  ASSERT_ERRNO_SUCCESS();
+  sched_param param;
+  param.sched_priority = 1;
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedparam(&attr, &param), 0);
+  ASSERT_ERRNO_SUCCESS();
+
+  SchedThreadArgs args;
+  pthread_t tid;
+  int res = LIBC_NAMESPACE::pthread_create(&tid, &attr, sched_runner, &args);
+  // Setting realtime scheduling policy SCHED_FIFO requires CAP_SYS_NICE or
+  // sufficient RLIMIT_RTPRIO. In unprivileged environments this will fail with
+  // EPERM, while in privileged environments it will succeed with 0.
+  ASSERT_TRUE(res == EPERM || res == 0);
+  ASSERT_ERRNO_SUCCESS();
+
+  if (res == 0) {
+    ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+    ASSERT_ERRNO_SUCCESS();
+    ASSERT_TRUE(args.executed.load());
+    ASSERT_EQ(args.policy, SCHED_FIFO);
+    ASSERT_EQ(args.priority, 1);
+  } else {
+    ASSERT_FALSE(args.executed.load());
+  }
+
+  ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_destroy(&attr), 0);
+  ASSERT_ERRNO_SUCCESS();
 }
 
 TEST_MAIN() {
   errno = 0;
   run_success_tests();
   run_failure_tests();
+  test_sched_inherit();
+  test_sched_explicit_success();
+  test_sched_realtime_permission();
   return 0;
 }



More information about the libc-commits mailing list