Skip to content

Commit 76f5fdc

Browse files
committed
Merge branch 'ab/tr2-leaks-and-fixes'
The tracing of process ancestry information has been enhanced. * ab/tr2-leaks-and-fixes: tr2: log N parent process names on Linux tr2: do compiler enum check in trace2_collect_process_info() tr2: leave the parent list empty upon failure & don't leak memory tr2: stop leaking "thread_name" memory tr2: clarify TRACE2_PROCESS_INFO_EXIT comment under Linux tr2: remove NEEDSWORK comment for "non-procfs" implementations
2 parents 11e5d0a + 2d3491b commit 76f5fdc

File tree

2 files changed

+146
-24
lines changed

2 files changed

+146
-24
lines changed

compat/linux/procinfo.c

Lines changed: 145 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,172 @@
44
#include "strvec.h"
55
#include "trace2.h"
66

7-
static void get_ancestry_names(struct strvec *names)
7+
/*
8+
* We need more complex parsing in stat_parent_pid() and
9+
* parse_proc_stat() below than a dumb fscanf(). That's because while
10+
* the statcomm field is surrounded by parentheses, the process itself
11+
* is free to insert any arbitrary byte sequence its its name. That
12+
* can include newlines, spaces, closing parentheses etc.
13+
*
14+
* See do_task_stat() in fs/proc/array.c in linux.git, this is in
15+
* contrast with the escaped version of the name found in
16+
* /proc/%d/status.
17+
*
18+
* So instead of using fscanf() we'll read N bytes from it, look for
19+
* the first "(", and then the last ")", anything in-between is our
20+
* process name.
21+
*
22+
* How much N do we need? On Linux /proc/sys/kernel/pid_max is 2^15 by
23+
* default, but it can be raised set to values of up to 2^22. So
24+
* that's 7 digits for a PID. We have 2 PIDs in the first four fields
25+
* we're interested in, so 2 * 7 = 14.
26+
*
27+
* We then have 3 spaces between those four values, and we'd like to
28+
* get to the space between the 4th and the 5th (the "pgrp" field) to
29+
* make sure we read the entire "ppid" field. So that brings us up to
30+
* 14 + 3 + 1 = 18. Add the two parentheses around the "comm" value
31+
* and it's 20. The "state" value itself is then one character (now at
32+
* 21).
33+
*
34+
* Finally the maximum length of the "comm" name itself is 15
35+
* characters, e.g. a setting of "123456789abcdefg" will be truncated
36+
* to "123456789abcdef". See PR_SET_NAME in prctl(2). So all in all
37+
* we'd need to read 21 + 15 = 36 bytes.
38+
*
39+
* Let's just read 2^6 (64) instead for good measure. If PID_MAX ever
40+
* grows past 2^22 we'll be future-proof. We'll then anchor at the
41+
* last ")" we find to locate the parent PID.
42+
*/
43+
#define STAT_PARENT_PID_READ_N 64
44+
45+
static int parse_proc_stat(struct strbuf *sb, struct strbuf *name,
46+
int *statppid)
847
{
48+
const char *comm_lhs = strchr(sb->buf, '(');
49+
const char *comm_rhs = strrchr(sb->buf, ')');
50+
const char *ppid_lhs, *ppid_rhs;
51+
char *p;
52+
pid_t ppid;
53+
54+
if (!comm_lhs || !comm_rhs)
55+
goto bad_kernel;
56+
57+
/*
58+
* We're at the ")", that's followed by " X ", where X is a
59+
* single "state" character. So advance by 4 bytes.
60+
*/
61+
ppid_lhs = comm_rhs + 4;
62+
63+
/*
64+
* Read until the space between the "ppid" and "pgrp" fields
65+
* to make sure we're anchored after the untruncated "ppid"
66+
* field..
67+
*/
68+
ppid_rhs = strchr(ppid_lhs, ' ');
69+
if (!ppid_rhs)
70+
goto bad_kernel;
71+
72+
ppid = strtol(ppid_lhs, &p, 10);
73+
if (ppid_rhs == p) {
74+
const char *comm = comm_lhs + 1;
75+
size_t commlen = comm_rhs - comm;
76+
77+
strbuf_add(name, comm, commlen);
78+
*statppid = ppid;
79+
80+
return 0;
81+
}
82+
83+
bad_kernel:
984
/*
10-
* NEEDSWORK: We could gather the entire pstree into an array to match
11-
* functionality with compat/win32/trace2_win32_process_info.c.
12-
* To do so, we may want to examine /proc/<pid>/stat. For now, just
13-
* gather the immediate parent name which is readily accessible from
14-
* /proc/$(getppid())/comm.
85+
* We were able to read our STAT_PARENT_PID_READ_N bytes from
86+
* /proc/%d/stat, but the content is bad. Broken kernel?
87+
* Should not happen, but handle it gracefully.
1588
*/
89+
return -1;
90+
}
91+
92+
static int stat_parent_pid(pid_t pid, struct strbuf *name, int *statppid)
93+
{
1694
struct strbuf procfs_path = STRBUF_INIT;
17-
struct strbuf name = STRBUF_INIT;
95+
struct strbuf sb = STRBUF_INIT;
96+
FILE *fp;
97+
int ret = -1;
1898

1999
/* try to use procfs if it's present. */
20-
strbuf_addf(&procfs_path, "/proc/%d/comm", getppid());
21-
if (strbuf_read_file(&name, procfs_path.buf, 0)) {
22-
strbuf_release(&procfs_path);
23-
strbuf_trim_trailing_newline(&name);
24-
strvec_push(names, strbuf_detach(&name, NULL));
25-
}
100+
strbuf_addf(&procfs_path, "/proc/%d/stat", pid);
101+
fp = fopen(procfs_path.buf, "r");
102+
if (!fp)
103+
goto cleanup;
104+
105+
/*
106+
* We could be more strict here and assert that we read at
107+
* least STAT_PARENT_PID_READ_N. My reading of procfs(5) is
108+
* that on any modern kernel (at least since 2.6.0 released in
109+
* 2003) even if all the mandatory numeric fields were zero'd
110+
* out we'd get at least 100 bytes, but let's just check that
111+
* we got anything at all and trust the parse_proc_stat()
112+
* function to handle its "Bad Kernel?" error checking.
113+
*/
114+
if (!strbuf_fread(&sb, STAT_PARENT_PID_READ_N, fp))
115+
goto cleanup;
116+
if (parse_proc_stat(&sb, name, statppid) < 0)
117+
goto cleanup;
118+
119+
ret = 0;
120+
cleanup:
121+
if (fp)
122+
fclose(fp);
123+
strbuf_release(&procfs_path);
124+
strbuf_release(&sb);
125+
126+
return ret;
127+
}
128+
129+
static void push_ancestry_name(struct strvec *names, pid_t pid)
130+
{
131+
struct strbuf name = STRBUF_INIT;
132+
int ppid;
133+
134+
if (stat_parent_pid(pid, &name, &ppid) < 0)
135+
goto cleanup;
136+
137+
strvec_push(names, name.buf);
138+
139+
/*
140+
* Both errors and reaching the end of the process chain are
141+
* reported as fields of 0 by proc(5)
142+
*/
143+
if (ppid)
144+
push_ancestry_name(names, ppid);
145+
cleanup:
146+
strbuf_release(&name);
26147

27148
return;
28-
/* NEEDSWORK: add non-procfs-linux implementations here */
29149
}
30150

31151
void trace2_collect_process_info(enum trace2_process_info_reason reason)
32152
{
33-
if (!trace2_is_enabled())
34-
return;
153+
struct strvec names = STRVEC_INIT;
35154

36-
/* someday we may want to write something extra here, but not today */
37-
if (reason == TRACE2_PROCESS_INFO_EXIT)
155+
if (!trace2_is_enabled())
38156
return;
39157

40-
if (reason == TRACE2_PROCESS_INFO_STARTUP) {
158+
switch (reason) {
159+
case TRACE2_PROCESS_INFO_EXIT:
41160
/*
42-
* NEEDSWORK: we could do the entire ptree in an array instead,
43-
* see compat/win32/trace2_win32_process_info.c.
161+
* The Windows version of this calls its
162+
* get_peak_memory_info() here. We may want to insert
163+
* similar process-end statistics here in the future.
44164
*/
45-
struct strvec names = STRVEC_INIT;
46-
47-
get_ancestry_names(&names);
165+
break;
166+
case TRACE2_PROCESS_INFO_STARTUP:
167+
push_ancestry_name(&names, getppid());
48168

49169
if (names.nr)
50170
trace2_cmd_ancestry(names.v);
51171
strvec_clear(&names);
172+
break;
52173
}
53174

54175
return;

trace2/tr2_tls.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ void tr2tls_unset_self(void)
9595

9696
pthread_setspecific(tr2tls_key, NULL);
9797

98+
strbuf_release(&ctx->thread_name);
9899
free(ctx->array_us_start);
99100
free(ctx);
100101
}

0 commit comments

Comments
 (0)