Expose a task subdirectory in each process directory and fill it with numeric 
thread index entries backed by libps thread proc_stats.

Thread nodes keep the containing process alive through the procfs parent 
reference chain. For now, only show the files that make sense for threads: stat 
and status use thread state plus process data from the containing process.

Signed-off-by: Bradley Morgan <[email protected]>
---
 procfs/Makefile  |   4 +-
 procfs/process.c | 220 +++++++++++++++++++++++++++++++++++++----------
 procfs/process.h |   4 +
 procfs/taskdir.c | 115 +++++++++++++++++++++++++
 procfs/taskdir.h |  21 +++++
 5 files changed, 316 insertions(+), 48 deletions(-)
 create mode 100644 procfs/taskdir.c
 create mode 100644 procfs/taskdir.h

diff --git a/procfs/Makefile b/procfs/Makefile
index d32328d2..1bb0b4b9 100644
--- a/procfs/Makefile
+++ b/procfs/Makefile
@@ -21,8 +21,8 @@ makemode := server
 
 target = procfs
 
-SRCS = procfs.c netfs.c procfs_dir.c process.c proclist.c rootdir.c dircat.c 
main.c mach_debugUser.c default_pagerUser.c pfinetUser.c
-LCLHDRS = dircat.h main.h process.h procfs.h procfs_dir.h proclist.h rootdir.h
+SRCS = procfs.c netfs.c procfs_dir.c process.c proclist.c rootdir.c taskdir.c 
dircat.c main.c mach_debugUser.c default_pagerUser.c pfinetUser.c
+LCLHDRS = dircat.h main.h process.h procfs.h procfs_dir.h proclist.h rootdir.h 
taskdir.h
 
 OBJS = $(SRCS:.c=.o)
 HURDLIBS = netfs fshelp iohelp ps ports ihash shouldbeinlibc
diff --git a/procfs/process.c b/procfs/process.c
index 3170b775..e24f5d33 100644
--- a/procfs/process.c
+++ b/procfs/process.c
@@ -28,6 +28,7 @@
 #include "procfs_dir.h"
 #include "process.h"
 #include "main.h"
+#include "taskdir.h"
 
 /* This module implements the process directories and the files they
    contain.  A libps proc_stat structure is created for each process
@@ -92,6 +93,33 @@ static int args_filename_length (const char *name)
   return strchrnul (name, ' ') - name;
 }
 
+static void
+get_code_range (struct proc_stat *ps, vm_address_t *start_code,
+               vm_address_t *end_code)
+{
+  process_t p;
+  error_t err;
+
+  *start_code = 1; /* 0 would make killall5.c consider it a kernel process,
+                     thus use 1 as default.  */
+  *end_code = 1;
+
+  err = proc_pid2proc (ps->context->server, ps->pid, &p);
+  if (! err)
+    {
+      boolean_t essential = 0;
+      proc_is_important (p, &essential);
+      if (essential)
+       *start_code = *end_code = 0; /* To make killall5.c consider it a
+                                       kernel process that is to be left
+                                       alone.  */
+      else
+       proc_get_code (p, start_code, end_code);
+
+      mach_port_deallocate (mach_task_self (), p);
+    }
+}
+
 /* Actual content generators */
 
 static ssize_t
@@ -216,20 +244,17 @@ process_file_gc_maps (struct proc_stat *ps, char 
**contents)
 }
 
 static ssize_t
-process_file_gc_stat (struct proc_stat *ps, char **contents)
+process_file_gc_stat_common (struct proc_stat *ps, struct proc_stat *task_ps,
+                            int pid, int num_threads, char **contents)
 {
-  struct procinfo *pi = proc_stat_proc_info (ps);
-  task_basic_info_t tbi = proc_stat_task_basic_info (ps);
+  struct procinfo *pi = proc_stat_proc_info (task_ps);
+  task_basic_info_t tbi = proc_stat_task_basic_info (task_ps);
   thread_basic_info_t thbi = proc_stat_thread_basic_info (ps);
   thread_sched_info_t thsi = proc_stat_thread_sched_info (ps);
-  const char *fn = args_filename (proc_stat_args (ps));
+  const char *fn = args_filename (proc_stat_args (task_ps));
 
-  vm_address_t start_code = 1; /* 0 would make killall5.c consider it
-                                 a kernel process, thus use 1 as
-                                 default.  */
-  vm_address_t end_code = 1;
-  process_t p;
-  error_t err = proc_pid2proc (ps->context->server, ps->pid, &p);
+  vm_address_t start_code;
+  vm_address_t end_code;
 
   unsigned last_processor;
 
@@ -239,19 +264,7 @@ process_file_gc_stat (struct proc_stat *ps, char 
**contents)
   last_processor = 0;
 #endif
 
-  if (! err)
-    {
-      boolean_t essential = 0;
-      proc_is_important (p, &essential);
-      if (essential)
-       start_code = end_code = 0; /* To make killall5.c consider it a
-                                     kernel process that is to be
-                                     left alone.  */
-      else
-       proc_get_code (p, &start_code, &end_code);
-
-      mach_port_deallocate (mach_task_self (), p);
-    }
+  get_code_range (task_ps, &start_code, &end_code);
 
   /* See proc(5) for more information about the contents of each field for the
      Linux procfs.  */
@@ -275,7 +288,7 @@ process_file_gc_stat (struct proc_stat *ps, char **contents)
       "%u %u "                 /* RT priority and policy */
       "%llu "                  /* aggregated block I/O delay */
       "\n",
-      proc_stat_pid (ps), args_filename_length (fn), fn, state_char (ps),
+      pid, args_filename_length (fn), fn, state_char (ps),
       pi->ppid, pi->pgrp, pi->session,
       0, 0,            /* no such thing as a major:minor for ctty */
       0,               /* no such thing as CLONE_* flags on Hurd */
@@ -285,7 +298,7 @@ process_file_gc_stat (struct proc_stat *ps, char **contents)
       0L, 0L,          /* cumulative time for children */
       MACH_PRIORITY_TO_NICE(thbi->base_priority) + 20,
       MACH_PRIORITY_TO_NICE(thbi->base_priority),
-      pi->nthreads, 0L,
+      num_threads, 0L,
       timeval_jiffies (tbi->creation_time), /* FIXME: ... since boot */
       (long unsigned) tbi->virtual_size,
       (long unsigned) tbi->resident_size / PAGE_SIZE, 0L,
@@ -301,6 +314,22 @@ process_file_gc_stat (struct proc_stat *ps, char 
**contents)
       0LL);
 }
 
+static ssize_t
+process_file_gc_stat (struct proc_stat *ps, char **contents)
+{
+  return process_file_gc_stat_common (ps, ps, proc_stat_pid (ps),
+                                    proc_stat_proc_info (ps)->nthreads,
+                                    contents);
+}
+
+static ssize_t
+process_file_gc_thread_stat (struct proc_stat *ps, char **contents)
+{
+  return process_file_gc_stat_common (ps, proc_stat_thread_origin (ps),
+                                    proc_stat_thread_index (ps), 1,
+                                    contents);
+}
+
 static ssize_t
 process_file_gc_statm (struct proc_stat *ps, char **contents)
 {
@@ -313,10 +342,11 @@ process_file_gc_statm (struct proc_stat *ps, char 
**contents)
 }
 
 static ssize_t
-process_file_gc_status (struct proc_stat *ps, char **contents)
+process_file_gc_status_common (struct proc_stat *ps, struct proc_stat *task_ps,
+                              int tgid, int pid, char **contents)
 {
-  task_basic_info_t tbi = proc_stat_task_basic_info (ps);
-  const char *fn = args_filename (proc_stat_args (ps));
+  task_basic_info_t tbi = proc_stat_task_basic_info (task_ps);
+  const char *fn = args_filename (proc_stat_args (task_ps));
 
   return asprintf (contents,
       "Name:\t%.*s\n"
@@ -332,18 +362,34 @@ process_file_gc_status (struct proc_stat *ps, char 
**contents)
       "Threads:\t%u\n",
       args_filename_length (fn), fn,
       state_string (ps),
-      proc_stat_pid (ps), /* XXX will need more work for threads */
-      proc_stat_pid (ps),
-      proc_stat_proc_info (ps)->ppid,
-      proc_stat_owner_uid (ps),
-      proc_stat_owner_uid (ps),
-      proc_stat_owner_uid (ps),
-      proc_stat_owner_uid (ps),
+      tgid,
+      pid,
+      proc_stat_proc_info (task_ps)->ppid,
+      proc_stat_owner_uid (task_ps),
+      proc_stat_owner_uid (task_ps),
+      proc_stat_owner_uid (task_ps),
+      proc_stat_owner_uid (task_ps),
       tbi->virtual_size / 1024,
       tbi->virtual_size / 1024,
       tbi->resident_size / 1024,
       tbi->resident_size / 1024,
-      proc_stat_num_threads (ps));
+      proc_stat_num_threads (task_ps));
+}
+
+static ssize_t
+process_file_gc_status (struct proc_stat *ps, char **contents)
+{
+  return process_file_gc_status_common (ps, ps, proc_stat_pid (ps),
+                                      proc_stat_pid (ps), contents);
+}
+
+static ssize_t
+process_file_gc_thread_status (struct proc_stat *ps, char **contents)
+{
+  struct proc_stat *task_ps = proc_stat_thread_origin (ps);
+
+  return process_file_gc_status_common (ps, task_ps, proc_stat_pid (task_ps),
+                                      proc_stat_thread_index (ps), contents);
 }
 
 
@@ -357,11 +403,20 @@ struct process_file_desc
   /* The proc_stat information required to get the contents of this file.  */
   ps_flags_t needs;
 
+  /* The proc_stat information required for thread nodes.  */
+  ps_flags_t thread_needs;
+
+  /* The containing process information required for thread nodes.  */
+  ps_flags_t thread_origin_needs;
+
   /* Content generator to use for this file.  Once we have acquired the
      necessary information, there can be only memory allocation errors,
      hence this simplified signature.  */
   ssize_t (*get_contents) (struct proc_stat *ps, char **contents);
 
+  /* Content generator to use for thread nodes.  */
+  ssize_t (*thread_get_contents) (struct proc_stat *ps, char **contents);
+
   /* The cmdline and environ contents don't need any cleaning since they
      point directly into the proc_stat structure.  */
   int no_cleanup;
@@ -381,17 +436,42 @@ static error_t
 process_file_get_contents (void *hook, char **contents, ssize_t *contents_len)
 {
   struct process_file_node *file = hook;
+  struct proc_stat *ps = file->ps;
+  struct proc_stat *origin = NULL;
+  ps_flags_t needs = file->desc->needs;
+  ssize_t (*get_contents) (struct proc_stat *, char **) =
+    file->desc->get_contents;
   error_t err;
 
+  if (proc_stat_is_thread (ps))
+    {
+      if (! file->desc->thread_get_contents)
+       return EIO;
+
+      needs = file->desc->thread_needs;
+      get_contents = file->desc->thread_get_contents;
+      origin = proc_stat_thread_origin (ps);
+    }
+
   /* Fetch the required information.  */
-  err = proc_stat_set_flags (file->ps, file->desc->needs);
+  err = proc_stat_set_flags (ps, needs);
   if (err)
     return EIO;
-  if ((proc_stat_flags (file->ps) & file->desc->needs) != file->desc->needs)
+  if ((proc_stat_flags (ps) & needs) != needs)
     return EIO;
 
+  if (origin && file->desc->thread_origin_needs)
+    {
+      err = proc_stat_set_flags (origin, file->desc->thread_origin_needs);
+      if (err)
+       return EIO;
+      if ((proc_stat_flags (origin) & file->desc->thread_origin_needs)
+         != file->desc->thread_origin_needs)
+       return EIO;
+    }
+
   /* Call the actual content generator (see the definitions below).  */
-  *contents_len = file->desc->get_contents (file->ps, contents);
+  *contents_len = get_contents (ps, contents);
   return 0;
 }
 
@@ -413,6 +493,7 @@ process_file_make_node (void *dir_hook, const void 
*entry_hook)
     .cleanup = free,
   };
   struct process_file_node *f;
+  struct proc_stat *owner_ps;
   struct node *np;
 
   f = malloc (sizeof *f);
@@ -426,7 +507,9 @@ process_file_make_node (void *dir_hook, const void 
*entry_hook)
   if (! np)
     return NULL;
 
-  procfs_node_chown (np, proc_stat_owner_uid (f->ps));
+  owner_ps = proc_stat_is_thread (f->ps)
+    ? proc_stat_thread_origin (f->ps) : f->ps;
+  procfs_node_chown (np, proc_stat_owner_uid (owner_ps));
   if (f->desc->mode)
     procfs_node_chmod (np, f->desc->mode);
 
@@ -451,6 +534,20 @@ process_stat_make_node (void *dir_hook, const void 
*entry_hook)
   return np;
 }
 
+static int
+process_file_exists (void *dir_hook, const void *entry_hook)
+{
+  const struct process_file_desc *desc = entry_hook;
+
+  return ! proc_stat_is_thread (dir_hook) || desc->thread_get_contents;
+}
+
+static int
+process_task_exists (void *dir_hook, const void *entry_hook)
+{
+  return ! proc_stat_is_thread (dir_hook);
+}
+
 
 /* Implementation of the process directory per se.  */
 
@@ -491,13 +588,25 @@ static struct procfs_dir_entry entries[] = {
       .mode = 0400,
     },
   },
+  {
+    .name = "task",
+    .ops = {
+      .make_node = taskdir_make_node,
+      .exists = process_task_exists,
+    },
+  },
   {
     .name = "stat",
     .hook = & (struct process_file_desc) {
       .get_contents = process_file_gc_stat,
+      .thread_get_contents = process_file_gc_thread_stat,
       .needs = PSTAT_PID | PSTAT_ARGS | PSTAT_STATE | PSTAT_PROC_INFO
        | PSTAT_TASK | PSTAT_TASK_BASIC | PSTAT_THREAD_BASIC
        | PSTAT_THREAD_SCHED | PSTAT_THREAD_WAIT,
+      .thread_needs = PSTAT_THREAD | PSTAT_STATE | PSTAT_THREAD_BASIC
+       | PSTAT_THREAD_SCHED | PSTAT_THREAD_WAIT,
+      .thread_origin_needs = PSTAT_PID | PSTAT_ARGS | PSTAT_PROC_INFO
+       | PSTAT_TASK_BASIC,
     },
     .ops = {
       .make_node = process_stat_make_node,
@@ -514,25 +623,46 @@ static struct procfs_dir_entry entries[] = {
     .name = "status",
     .hook = & (struct process_file_desc) {
       .get_contents = process_file_gc_status,
+      .thread_get_contents = process_file_gc_thread_status,
       .needs = PSTAT_PID | PSTAT_ARGS | PSTAT_STATE | PSTAT_PROC_INFO
         | PSTAT_TASK_BASIC | PSTAT_OWNER_UID | PSTAT_NUM_THREADS,
+      .thread_needs = PSTAT_THREAD | PSTAT_STATE | PSTAT_THREAD_BASIC,
+      .thread_origin_needs = PSTAT_PID | PSTAT_ARGS | PSTAT_PROC_INFO
+        | PSTAT_TASK_BASIC | PSTAT_OWNER_UID | PSTAT_NUM_THREADS,
     },
   },
   {}
 };
 
-error_t
-process_lookup_pid (struct ps_context *pc, pid_t pid, struct node **np)
+struct node *
+process_make_node (struct proc_stat *ps)
 {
   static const struct procfs_dir_ops dir_ops = {
     .entries = entries,
     .cleanup = (void (*)(void *)) _proc_stat_free,
     .entry_ops = {
       .make_node = process_file_make_node,
+      .exists = process_file_exists,
     },
   };
-  struct proc_stat *ps;
+  struct proc_stat *owner_ps = proc_stat_is_thread (ps)
+    ? proc_stat_thread_origin (ps) : ps;
+  struct node *np;
   int owner;
+
+  np = procfs_dir_make_node (&dir_ops, ps);
+  if (! np)
+    return NULL;
+
+  owner = proc_stat_owner_uid (owner_ps);
+  procfs_node_chown (np, owner >= 0 ? owner : opt_anon_owner);
+  return np;
+}
+
+error_t
+process_lookup_pid (struct ps_context *pc, pid_t pid, struct node **np)
+{
+  struct proc_stat *ps;
   error_t err;
 
   err = _proc_stat_create (pid, pc, &ps);
@@ -548,11 +678,9 @@ process_lookup_pid (struct ps_context *pc, pid_t pid, 
struct node **np)
       return EIO;
     }
 
-  *np = procfs_dir_make_node (&dir_ops, ps);
+  *np = process_make_node (ps);
   if (! *np)
     return ENOMEM;
 
-  owner = proc_stat_owner_uid (ps);
-  procfs_node_chown (*np, owner >= 0 ? owner : opt_anon_owner);
   return 0;
 }
diff --git a/procfs/process.h b/procfs/process.h
index b230a281..b3c41359 100644
--- a/procfs/process.h
+++ b/procfs/process.h
@@ -25,3 +25,7 @@
 error_t
 process_lookup_pid (struct ps_context *pc, pid_t pid, struct node **np);
 
+/* Create a node for a process or thread proc_stat.  The returned node
+   consumes PS.  */
+struct node *
+process_make_node (struct proc_stat *ps);
diff --git a/procfs/taskdir.c b/procfs/taskdir.c
new file mode 100644
index 00000000..677d2404
--- /dev/null
+++ b/procfs/taskdir.c
@@ -0,0 +1,115 @@
+/* Hurd /proc filesystem, implementation of process thread directories.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is part of the GNU Hurd.
+
+   The GNU Hurd is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2, or (at
+   your option) any later version.
+
+   The GNU Hurd is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ps.h>
+#include "procfs.h"
+#include "process.h"
+#include "main.h"
+
+#define THREAD_INDEX_STR_SIZE (3 * sizeof (unsigned int) + 1)
+
+static error_t
+taskdir_get_contents (void *hook, char **contents, ssize_t *contents_len)
+{
+  static const char dot_dotdot[] = ".\0..";
+  struct proc_stat *ps = hook;
+  error_t err;
+  unsigned int i;
+  size_t pos;
+
+  err = proc_stat_set_flags (ps, PSTAT_NUM_THREADS);
+  if (err)
+    return err;
+  if (! (proc_stat_flags (ps) & PSTAT_NUM_THREADS))
+    return EIO;
+
+  *contents = malloc (sizeof dot_dotdot
+                     + proc_stat_num_threads (ps) * THREAD_INDEX_STR_SIZE);
+  if (! *contents)
+    return ENOMEM;
+
+  memcpy (*contents, dot_dotdot, sizeof dot_dotdot);
+  pos = sizeof dot_dotdot;
+
+  for (i = 0; i < proc_stat_num_threads (ps); i++)
+    {
+      int n = sprintf (*contents + pos, "%u", i);
+      assert_backtrace (n >= 0);
+      pos += n + 1;
+    }
+
+  *contents_len = pos;
+  return 0;
+}
+
+static error_t
+taskdir_lookup (void *hook, const char *name, struct node **np)
+{
+  struct proc_stat *ps = hook;
+  struct proc_stat *thread_ps;
+  unsigned long index;
+  char *endp;
+  error_t err;
+
+  if (name[0] == '\0' || (name[0] == '0' && name[1] != '\0'))
+    return ENOENT;
+
+  errno = 0;
+  index = strtoul (name, &endp, 10);
+  if (errno || *endp || index > UINT_MAX)
+    return ENOENT;
+
+  err = proc_stat_thread_create (ps, index, &thread_ps);
+  if (err == EINVAL)
+    return ENOENT;
+  if (err)
+    return err;
+
+  *np = process_make_node (thread_ps);
+  if (! *np)
+    return ENOMEM;
+
+  return 0;
+}
+
+struct node *
+taskdir_make_node (void *dir_hook, const void *entry_hook)
+{
+  static const struct procfs_node_ops ops = {
+    .get_contents = taskdir_get_contents,
+    .lookup = taskdir_lookup,
+    .cleanup_contents = procfs_cleanup_contents_with_free,
+  };
+  struct proc_stat *ps = dir_hook;
+  struct node *np;
+  int owner;
+
+  np = procfs_make_node (&ops, ps);
+  if (! np)
+    return NULL;
+
+  owner = proc_stat_owner_uid (ps);
+  procfs_node_chown (np, owner >= 0 ? owner : opt_anon_owner);
+  return np;
+}
diff --git a/procfs/taskdir.h b/procfs/taskdir.h
new file mode 100644
index 00000000..0c5ff47f
--- /dev/null
+++ b/procfs/taskdir.h
@@ -0,0 +1,21 @@
+/* Hurd /proc filesystem, implementation of process thread directories.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is part of the GNU Hurd.
+
+   The GNU Hurd is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2, or (at
+   your option) any later version.
+
+   The GNU Hurd is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
+
+struct node *
+taskdir_make_node (void *dir_hook, const void *entry_hook);
-- 
2.53.0


Reply via email to