Skip to contentSkip to conversation

taskstats: fix TGID exit version and tool message truncation

PATCH Applied Yiyang Chen 2 participants 5 messages Mar 29 – Mar 31

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

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
#1 lore raw

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.

Fixes ad4ecbcba728 ("[PATCH] delay accounting taskstats interface send tgid once")
---
 kernel/taskstats.c | 1 +
 1 file changed, 1 insertion(+)
kernel/taskstats.c +1 -0
index 0cd680ccc7e5..73bd6a6a7893 100644
@@ -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
maintainer
#2 lore raw

Re: [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() 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.

Quoted 5 lines

Set stats->version = TASKSTATS_VERSION after 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);
#3 lore raw

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() 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.

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

#4 lore raw

PATCH 2/2 tools/accounting: handle truncated taskstats netlink messages

In reply to Yiyang Chen

procacct 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
index 50792df27707..368a622ca027 100644
@@ -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
index e8dee05a6264..46e5986ad927 100644
@@ -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