taskstats: fix TGID exit version and tool message truncation
cover taskstats: fix TGID exit version and tool message truncation
This series contains two independent fixes around taskstats.
The first patch fixes a long-standing taskstats bug where TGID exit notifications can carry version == 0 because the cached signal->stats aggregate is copied into the outgoing payload without restoring the taskstats version field.
The second patch hardens the accounting sample tools against truncated taskstats netlink messages by switching to recvmsg(), checking MSG_TRUNC explicitly, and increasing the receive buffer size.
Yiyang Chen (2):
taskstats: set version in TGID exit notifications tools/accounting: handle truncated taskstats netlink messages
kernel/taskstats.c | 1 + tools/accounting/getdelays.c | 41 ++++++++++++++++++++++++++++++++---- tools/accounting/procacct.c | 40 +++++++++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 8 deletions(-)
base-commit: f242ac4a09443c6e2e0ec03d7e2a21b00cbb3907
Signature · 1 line
2.43.0
PATCH 1/2 taskstats: set version in TGID exit notifications
delay accounting started populating taskstats records with a valid version field via fill_pid() and fill_tgid().
Later, commit ad4ecbcba728 ("[PATCH] delay accounting taskstats interface send tgid once") changed the TGID exit path to send the cached signal->stats aggregate directly instead of building the outgoing record through fill_tgid(). Unlike fill_tgid(), fill_tgid_exit() only accumulates accounting data and never initializes stats->version.
As a result, TGID exit notifications can reach userspace with version == 0 even though PID exit notifications and TASKSTATS_CMD_GET replies carry a valid taskstats version.
Set stats->version = TASKSTATS_VERSION after copying the cached TGID aggregate into the outgoing netlink payload so all taskstats records are self-describing again.
--- kernel/taskstats.c | 1 + 1 file changed, 1 insertion(+)
kernel/taskstats.c +1 -0
@@ -649,6 +649,7 @@ void taskstats_exit(struct task_struct *tsk, int group_dead) goto err;memcpy (stats, tsk->signal->stats,sizeof (*stats)); + stats->version =TASKSTATS_VERSION ; send:send_cpu_listeners (rep_skb, listeners);
Signature · 1 line
2.43.0
Andrew Morton
maintainerRe: [PATCH 1/2] taskstats: set version in TGID exit notifications
Quoted 13 lines · On Mon, 30 Mar 2026 03:00:40 +0800 Yiyang Chen <[email protected]> wrote:
delay accounting started populating taskstats records with a valid version field via
fill_pid()andfill_tgid().Later, commit
ad4ecbcba728("[PATCH] delay accounting taskstats interface send tgid once") changed the TGID exit path to send the cachedsignal->statsaggregate directly instead of building the outgoing record throughfill_tgid(). Unlikefill_tgid(),fill_tgid_exit()only accumulates accounting data and never initializesstats->version.As a result, TGID exit notifications can reach userspace with version == 0 even though PID exit notifications and
TASKSTATS_CMD_GETreplies carry a valid taskstats version.
Quoted 5 lines
Set
stats->version=TASKSTATS_VERSIONafter copying the cached TGID aggregate into the outgoing netlink payload so all taskstats records are self-describing again.Fixes ad4ecbcba728 ("[PATCH] delay accounting taskstats interface send tgid once")
Thanks, lol, 20 years ago.
Can you explain how others can trigger this? Some combination of steps which results in the bad output?
Quoted 1 line
Is there a chance of breaking existing userspace here? Some existing userspace code which is expecting 0 here and will get surprised by this change?
Quoted 10 lines
kernel/taskstats.c +1
-0@@ -649,6 +649,7 @@ void taskstats_exit(struct task_struct *tsk, int group_dead) goto err;memcpy (stats, tsk->signal->stats,sizeof (*stats)); + stats->version =TASKSTATS_VERSION ; send:send_cpu_listeners (rep_skb, listeners);
Re: [PATCH 1/2] taskstats: set version in TGID exit notifications
Quoted 20 lines · Andrew Morton
Quoted 17 lines · On Mon, 30 Mar 2026 03:00:40 +0800 Yiyang Chen <[email protected]> wrote:
delay accounting started populating taskstats records with a valid version field via
fill_pid()andfill_tgid().Later, commit
ad4ecbcba728("[PATCH] delay accounting taskstats interface send tgid once") changed the TGID exit path to send the cachedsignal->statsaggregate directly instead of building the outgoing record throughfill_tgid(). Unlikefill_tgid(),fill_tgid_exit()only accumulates accounting data and never initializesstats->version.As a result, TGID exit notifications can reach userspace with version == 0 even though PID exit notifications and
TASKSTATS_CMD_GETreplies carry a valid taskstats version.Set
stats->version=TASKSTATS_VERSIONafter copying the cached TGID aggregate into the outgoing netlink payload so all taskstats records are self-describing again.
Quoted 6 lines
Quoted 1 line
Fixes ad4ecbcba728 ("[PATCH] delay accounting taskstats interface send tgid once")Thanks, lol, 20 years ago.
Can you explain how others can trigger this? Some combination of steps which results in the bad output?
Yes. This is easy to reproduce with tools/accounting/getdelays.c.
I have a small follow-up patch for that tool which:
1. increases the receive buffer/message size so the pid+tgid combined exit notification is not dropped/truncated
2. prints stats->version.
With that patch, the reproducer is:
Terminal 1:
./getdelays -d -v -l -m 0
Terminal 2:
taskset -c 0 python3 -c 'import threading,time; t=threading.Thread(target=time.sleep,args=(0.1,)); t.start(); t.join()'
That produces both PID and TGID exit notifications for the same process. The PID exit record reports a valid taskstats version, while the TGID exit record reports version 0.
Quoted 6 lines
Quoted 2 lines
Is there a chance of breaking existing userspace here? Some existing userspace code which is expecting 0 here and will get surprised by this change?
In practice, userspace uses taskstats.version to decide which fields are present in struct taskstats, i.e. as a schema/version discriminator. A zero version does not describe a valid taskstats layout, so it is hard to see how userspace could use 0 as a meaningful or useful distinction here.
So I do not think fixing this in mainline should break sensible userspace. It just restores consistency of the taskstats version semantics across TASKSTATS_CMD_GET, PID exit notifications, and TGID exit notifications.
To be honest, I'm also not sure if this should backport to stable. But I think mainline should still fix it.
Thanks,
Yiyang
PATCH 2/2 tools/accounting: handle truncated taskstats netlink messages
In reply to Yiyang Chenprocacct and getdelays use a fixed receive buffer for taskstats generic netlink messages. A multi-threaded process exit can emit a single PID+TGID notification large enough to exceed that buffer on newer kernels.
Switch to recvmsg() so MSG_TRUNC is detected explicitly, increase the message buffer size, and report truncated datagrams clearly instead of misparsing them as fatal netlink errors.
Also print the taskstats version in debug output to make version mismatches easier to diagnose while inspecting taskstats traffic.
--- tools/accounting/getdelays.c | 41 ++++++++++++++++++++++++++++++++---- tools/accounting/procacct.c | 40 +++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 8 deletions(-)
tools/accounting/getdelays.c +37 -4
@@ -60,7 +60,7 @@ int print_task_context_switch_counts; }/* Maximum size of response requested or message sent */ -#define MAX_MSG_SIZE 1024 +#define MAX_MSG_SIZE 2048/* Maximum number of cpus expected to be specified in a cpumask */ #define MAX_CPUS 32
@@ -115,6 +115,32 @@ static int create_nl_socket(int protocol)return -1 ; } +static int recv_taskstats_msg (int sd,struct msgtemplate *msg) +{ +struct sockaddr_nl nladdr; +struct iovec iov = { + .iov_base = msg, + .iov_len =sizeof (*msg), + }; +struct msghdr hdr = { + .msg_name = &nladdr, + .msg_namelen =sizeof (nladdr), + .msg_iov = &iov, + .msg_iovlen =1 , + }; +int ret; + + ret =recvmsg (sd, &hdr,0 ); +if (ret <0 ) +return -1 ; +if (hdr.msg_flags &MSG_TRUNC ) { + errno =EMSGSIZE ; +return -1 ; + } + +return ret; +} + static int send_cmd(int sd, __u16 nlmsg_type, __u32 nlmsg_pid, __u8 genl_cmd, __u16 nla_type,
@@ -633,12 +659,16 @@ int main(int argc, char *argv[]) } do { - rep_len = recv(nl_sd, &msg, sizeof(msg), 0); + rep_len = recv_taskstats_msg(nl_sd, &msg); PRINTF("received %d bytes\n", rep_len); if (rep_len < 0) { - fprintf(stderr, "nonfatal reply error: errno %d\n", - errno); + if (errno == EMSGSIZE) + fprintf(stderr, + "dropped truncated taskstats netlink message, please increase MAX_MSG_SIZE\n"); + else + fprintf(stderr, "nonfatal reply error: errno %d\n", + errno); continue; } if (msg.n.nlmsg_type == NLMSG_ERROR ||
@@ -680,6 +710,9 @@ int main(int argc, char *argv[])printf ("TGID\t%d\n" , rtid);break ;case TASKSTATS_TYPE_STATS : +PRINTF ("version %u\n" , + ((struct taskstats *) +NLA_DATA (na))->version);if (print_delays)print_delayacct ((struct taskstats *)NLA_DATA (na));if (print_io_accounting)
tools/accounting/procacct.c +36 -4
@@ -71,7 +71,7 @@ int print_task_context_switch_counts; }/* Maximum size of response requested or message sent */ -#define MAX_MSG_SIZE 1024 +#define MAX_MSG_SIZE 2048/* Maximum number of cpus expected to be specified in a cpumask */ #define MAX_CPUS 32
@@ -121,6 +121,32 @@ static int create_nl_socket(int protocol)return -1 ; } +static int recv_taskstats_msg (int sd,struct msgtemplate *msg) +{ +struct sockaddr_nl nladdr; +struct iovec iov = { + .iov_base = msg, + .iov_len =sizeof (*msg), + }; +struct msghdr hdr = { + .msg_name = &nladdr, + .msg_namelen =sizeof (nladdr), + .msg_iov = &iov, + .msg_iovlen =1 , + }; +int ret; + + ret =recvmsg (sd, &hdr,0 ); +if (ret <0 ) +return -1 ; +if (hdr.msg_flags &MSG_TRUNC ) { + errno =EMSGSIZE ; +return -1 ; + } + +return ret; +} + static int send_cmd(int sd, __u16 nlmsg_type, __u32 nlmsg_pid, __u8 genl_cmd, __u16 nla_type,
@@ -239,6 +265,8 @@ void handle_aggr(int mother, struct nlattr *na, int fd)PRINTF ("TGID\t%d\n" , rtid);break ;case TASKSTATS_TYPE_STATS : +PRINTF ("version %u\n" , + ((struct taskstats *)NLA_DATA (na))->version);if (mother ==TASKSTATS_TYPE_AGGR_PID )print_procacct ((struct taskstats *)NLA_DATA (na));if (fd) {
@@ -347,12 +375,16 @@ int main(int argc, char *argv[]) } do { - rep_len = recv(nl_sd, &msg, sizeof(msg), 0); + rep_len = recv_taskstats_msg(nl_sd, &msg); PRINTF("received %d bytes\n", rep_len); if (rep_len < 0) { - fprintf(stderr, "nonfatal reply error: errno %d\n", - errno); + if (errno == EMSGSIZE) + fprintf(stderr, + "dropped truncated taskstats netlink message, please increase MAX_MSG_SIZE\n"); + else + fprintf(stderr, "nonfatal reply error: errno %d\n", + errno); continue; } if (msg.n.nlmsg_type == NLMSG_ERROR ||
Signature · 1 line
2.43.0