]> git.ipfire.org Git - thirdparty/shadow.git/commitdiff
userdel: fix user busy detection for threads
authorTobias Deiminger <tobias.deiminger@linutronix.de>
Wed, 6 May 2026 12:39:39 +0000 (14:39 +0200)
committerAlejandro Colomar <foss+github@alejandro-colomar.es>
Tue, 19 May 2026 12:46:55 +0000 (14:46 +0200)
On Linux, userdel/usermod check all /proc/<pid> status files to ensure a
to-be-modified user has no more running tasks, or abort modification
otherwise.

However, the check failed to detect threads running as the user if the
corresponding main thread ran as a different user. The user is deleted
despite still being busy. This is due to passing a wrong value to
check_status. The caller passed "<pid>/task", rather than
"<pid>/task/<tid>". In consequence check_status tried to open
"/proc/<pid>/task/status" - a wrong path that never exists - open fails,
and check_status always returns 0. The correct status file name would
have been "/proc/<pid>/task/<tid>/status" instead.

The bug can only be reproduced by rather exotic code using raw syscalls.
POSIX does not allow threads to have different UIDs.

To fix it, construct the correct path to the tid status file in the
caller, before passing it to check_status.

Reproducer:

  // setuid_thread.c

  #include <pthread.h>
  #include <pwd.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <sys/syscall.h>
  #include <unistd.h>

  static uid_t target_uid;

  static void *user_thread(void *arg)
  {
          syscall(SYS_setuid, (long)target_uid);
          for (;;) {
                  printf("thread running as uid %d (pid=%d)\n", (int)target_uid,
                         (int)getpid());
                  sleep(5);
          }
          return NULL;
  }

  int main(int argc, char *argv[])
  {
          if (argc < 2) {
                  fprintf(stderr, "Usage: %s <username>\n", argv[0]);
                  return 1;
          }

          struct passwd *pw = getpwnam(argv[1]);
          if (!pw) {
                  fprintf(stderr, "user not found: %s\n", argv[1]);
                  return 1;
          }
          target_uid = pw->pw_uid;

          pthread_t tid;
          pthread_create(&tid, NULL, user_thread, NULL);
          sleep(60);
          return 0;
  }

Execute in a shell

  gcc setuid_thread.c -o setuid_thread
  sudo useradd --no-create-home testuser
  sudo ./setuid_thread testuser &
  sudo userdel testuser

Behavior without fix:
No output, testuser is deleted.

Behavior with fix:
Output "userdel: user testuser is currently used by process 178863".
testuser is not deleted.

Signed-off-by: Tobias Deiminger <tobias.deiminger@linutronix.de>
lib/user_busy.c

index d157b5749558d4100169579a4062f0a8bc9c36ed..9e5ff78baac805059877b483b6e9c146c035a9a4 100644 (file)
@@ -241,13 +241,17 @@ static int user_busy_processes (const char *name, uid_t uid)
                if (task_dir != NULL) {
                        while (NULL != (ent = readdir(task_dir))) {
                                pid_t tid;
+                               /* 27: xxxxxxxxxx/task/xxxxxxxxxx + \0 */
+                               char  tid_sname[27];
+
                                if (get_pid(ent->d_name, &tid) == -1) {
                                        continue;
                                }
                                if (tid == pid) {
                                        continue;
                                }
-                               if (check_status (name, task_path+6, uid) != 0) {
+                               stprintf_a(tid_sname, "%ld/task/%ld", (long) pid, (long) tid);
+                               if (check_status (name, tid_sname, uid) != 0) {
                                        (void) closedir (proc);
                                        (void) closedir (task_dir);
 #ifdef ENABLE_SUBIDS
@@ -272,4 +276,3 @@ static int user_busy_processes (const char *name, uid_t uid)
        return 0;
 }
 #endif                         /* __linux__ */
-