1/*
2 * New Interface to Process Table -- PROCTAB Stream (a la Directory streams)
3 * Copyright (C) 1996 Charles L. Blake.
4 * Copyright (C) 1998 Michael K. Johnson
5 * Copyright 1998-2002 Albert Cahalan
6 * May be distributed under the conditions of the
7 * GNU Library General Public License; a copy is in COPYING
8 */
9#ifdef HAVE_CONFIG_H
10#include "config.h"
11#endif
12#include "version.h"
13#include "readproc.h"
14#include "alloc.h"
15#include "pwcache.h"
16#include "devname.h"
17#include "procps.h"
18#include <stdio.h>
19#include <stdlib.h>
20#include <errno.h>
21#include <stdarg.h>
22#include <string.h>
23#include <unistd.h>
24#include <signal.h>
25#include <fcntl.h>
26#include <sys/dir.h>
27#include <sys/types.h>
28#include <sys/stat.h>
29
30#ifdef FLASK_LINUX
31#include <fs_secure.h>
32#endif
33
34/* initiate a process table scan
35 */
36PROCTAB *openproc(int flags, ...)
37{
38	va_list ap;
39	PROCTAB *PT = xmalloc(sizeof(PROCTAB));
40
41	if (flags & PROC_PID)
42		PT->procfs = NULL;
43	else if (!(PT->procfs = opendir("/proc")))
44		return NULL;
45	PT->flags = flags;
46	va_start(ap, flags);	/*  Init args list */
47	if (flags & PROC_PID)
48		PT->pids = va_arg(ap, pid_t *);
49	else if (flags & PROC_UID) {
50		PT->uids = va_arg(ap, uid_t *);
51		PT->nuid = va_arg(ap, int);
52	}
53	va_end(ap);		/*  Clean up args list */
54	return PT;
55}
56
57/* terminate a process table scan
58 */
59void closeproc(PROCTAB * PT)
60{
61	if (PT) {
62		if (PT->procfs)
63			closedir(PT->procfs);
64		free(PT);
65	}
66}
67
68/* deallocate the space allocated by readproc if the passed rbuf was NULL
69 */
70void freeproc(proc_t * p)
71{
72	if (!p)			/* in case p is NULL */
73		return;
74	/* ptrs are after strings to avoid copying memory when building them. */
75	/* so free is called on the address of the address of strvec[0]. */
76	if (p->cmdline)
77		free((void *)*p->cmdline);
78	if (p->environ)
79		free((void *)*p->environ);
80	free(p);
81}
82
83// 2.5.xx looks like:
84//
85// "State:\t%s\n"
86// "Tgid:\t%d\n"
87// "Pid:\t%d\n"
88// "PPid:\t%d\n"
89// "TracerPid:\t%d\n"
90
91static void status2proc(const char *S, proc_t * restrict P)
92{
93	char *tmp;
94	unsigned i;
95
96	// The cmd is escaped, with \\ and \n for backslash and newline.
97	// It certainly may contain "VmSize:" and similar crap.
98	if (unlikely(strncmp("Name:\t", S, 6)))
99		fprintf(stderr, "Internal error!\n");
100	S += 6;
101	i = 0;
102	while (i < sizeof P->cmd - 1) {
103		int c = *S++;
104		if (unlikely(c == '\n'))
105			break;
106		if (unlikely(c == '\0'))
107			return;	// should never happen
108		if (unlikely(c == '\\')) {
109			c = *S++;
110			if (c == '\n')
111				break;	// should never happen
112			if (!c)
113				break;	// should never happen
114			if (c == 'n')
115				c = '\n';	// else we assume it is '\\'
116		}
117		P->cmd[i++] = c;
118	}
119	P->cmd[i] = '\0';
120
121	tmp = strstr(S, "State:\t");
122	if (likely(tmp))
123		P->state = tmp[7];
124	else
125		fprintf(stderr, "Internal error!\n");
126
127	tmp = strstr(S, "PPid:");
128	if (likely(tmp))
129		sscanf(tmp, "PPid:\t%d\n", &P->ppid);
130	else
131		fprintf(stderr, "Internal error!\n");
132
133	tmp = strstr(S, "Uid:");
134	if (likely(tmp))
135		sscanf(tmp,
136		       "Uid:\t%d\t%d\t%d\t%d",
137		       &P->ruid, &P->euid, &P->suid, &P->fuid);
138	else
139		fprintf(stderr, "Internal error!\n");
140
141	tmp = strstr(S, "Gid:");
142	if (likely(tmp))
143		sscanf(tmp,
144		       "Gid:\t%d\t%d\t%d\t%d",
145		       &P->rgid, &P->egid, &P->sgid, &P->fgid);
146	else
147		fprintf(stderr, "Internal error!\n");
148
149	tmp = strstr(S, "VmSize:");
150	if (likely(tmp))
151		sscanf(tmp,
152		       "VmSize: %lu kB\n"
153		       "VmLck: %lu kB\n"
154		       "VmRSS: %lu kB\n"
155		       "VmData: %lu kB\n"
156		       "VmStk: %lu kB\n"
157		       "VmExe: %lu kB\n"
158		       "VmLib: %lu kB\n",
159		       &P->vm_size, &P->vm_lock, &P->vm_rss, &P->vm_data,
160		       &P->vm_stack, &P->vm_exe, &P->vm_lib);
161	else {			/* looks like an annoying kernel thread */
162
163		P->vm_size = 0;
164		P->vm_lock = 0;
165		P->vm_rss = 0;
166		P->vm_data = 0;
167		P->vm_stack = 0;
168		P->vm_exe = 0;
169		P->vm_lib = 0;
170	}
171
172	tmp = strstr(S, "SigPnd:");
173	if (likely(tmp))
174		sscanf(tmp,
175#ifdef SIGNAL_STRING
176		       "SigPnd: %s SigBlk: %s SigIgn: %s %*s %s",
177		       P->signal, P->blocked, P->sigignore, P->sigcatch
178#else
179		       "SigPnd: %Lx SigBlk: %Lx SigIgn: %Lx %*s %Lx",
180		       &P->signal, &P->blocked, &P->sigignore, &P->sigcatch
181#endif
182		    );
183	else
184		fprintf(stderr, "Internal error!\n");
185}
186
187// Reads /proc/*/stat files, being careful not to trip over processes with
188// names like ":-) 1 2 3 4 5 6".
189static void stat2proc(const char *S, proc_t * restrict P)
190{
191	unsigned num;
192	char *tmp;
193
194	/* fill in default values for older kernels */
195	P->exit_signal = SIGCHLD;
196	P->processor = 0;
197	P->rtprio = -1;
198	P->sched = -1;
199
200	S = strchr(S, '(') + 1;
201	tmp = strrchr(S, ')');
202	num = tmp - S;
203	if (unlikely(num >= sizeof P->cmd))
204		num = sizeof P->cmd - 1;
205	memcpy(P->cmd, S, num);
206	P->cmd[num] = '\0';
207	S = tmp + 2;		// skip ") "
208
209	num = sscanf(S, "%c " "%d %d %d %d %d " "%lu %lu %lu %lu %lu " "%Lu %Lu %Lu %Lu "	/* utime stime cutime cstime */
210		     "%ld %ld %ld %ld " "%Lu "	/* start_time */
211		     "%lu " "%ld " "%lu %lu %lu %lu %lu %lu " "%*s %*s %*s %*s "	/* discard, no RT signals & Linux 2.1 used hex */
212		     "%lu %lu %lu "
213		     "%d %d "
214		     "%lu %lu",
215		     &P->state,
216		     &P->ppid, &P->pgrp, &P->session, &P->tty, &P->tpgid,
217		     &P->flags, &P->min_flt, &P->cmin_flt, &P->maj_flt,
218		     &P->cmaj_flt, &P->utime, &P->stime, &P->cutime, &P->cstime,
219		     &P->priority, &P->nice, &P->timeout, &P->it_real_value,
220		     &P->start_time, &P->vsize, &P->rss, &P->rss_rlim,
221		     &P->start_code, &P->end_code, &P->start_stack,
222		     &P->kstk_esp, &P->kstk_eip,
223		     /*     P->signal, P->blocked, P->sigignore, P->sigcatch,   *//* can't use */
224		     &P->wchan, &P->nswap, &P->cnswap,
225/* -- Linux 2.0.35 ends here -- */
226		     &P->exit_signal, &P->processor,	/* 2.2.1 ends with "exit_signal" */
227/* -- Linux 2.2.8 to 2.5.17 end here -- */
228		     &P->rtprio, &P->sched	/* both added to 2.5.18 */
229	    );
230}
231
232static void statm2proc(const char *s, proc_t * restrict P)
233{
234	int num;
235	num = sscanf(s, "%ld %ld %ld %ld %ld %ld %ld",
236		     &P->size, &P->resident, &P->share,
237		     &P->trs, &P->lrs, &P->drs, &P->dt);
238/*    fprintf(stderr, "statm2proc converted %d fields.\n",num); */
239}
240
241static int file2str(const char *directory, const char *what, char *ret, int cap)
242{
243	static char filename[80];
244	int fd, num_read;
245
246	sprintf(filename, "%s/%s", directory, what);
247	fd = open(filename, O_RDONLY, 0);
248	if (unlikely(fd == -1))
249		return -1;
250	num_read = read(fd, ret, cap - 1);
251	if (unlikely(num_read <= 0))
252		num_read = -1;
253	else
254		ret[num_read] = 0;
255	close(fd);
256	return num_read;
257}
258
259static char **file2strvec(const char *directory, const char *what)
260{
261	char buf[2048];		/* read buf bytes at a time */
262	char *p, *rbuf = 0, *endbuf, **q, **ret;
263	int fd, tot = 0, n, c, end_of_file = 0;
264	int align;
265
266	sprintf(buf, "%s/%s", directory, what);
267	fd = open(buf, O_RDONLY, 0);
268	if (fd == -1)
269		return NULL;
270
271	/* read whole file into a memory buffer, allocating as we go */
272	while ((n = read(fd, buf, sizeof buf - 1)) > 0) {
273		if (n < (int)(sizeof buf - 1))
274			end_of_file = 1;
275		if (n == 0 && rbuf == 0)
276			return NULL;	/* process died between our open and read */
277		if (n < 0) {
278			free(rbuf);
279			return NULL;	/* read error */
280		}
281		if (end_of_file && buf[n - 1])	/* last read char not null */
282			buf[n++] = '\0';	/* so append null-terminator */
283		rbuf = xrealloc(rbuf, tot + n);	/* allocate more memory */
284		memcpy(rbuf + tot, buf, n);	/* copy buffer into it */
285		tot += n;	/* increment total byte ctr */
286		if (end_of_file)
287			break;
288	}
289	close(fd);
290	if (n <= 0 && !end_of_file) {
291		free(rbuf);
292		return NULL;	/* read error */
293	}
294	endbuf = rbuf + tot;	/* count space for pointers */
295	align =
296	    (sizeof(char *) - 1) -
297	    ((tot + sizeof(char *) - 1) & (sizeof(char *) - 1));
298	for (c = 0, p = rbuf; p < endbuf; p++)
299		if (!*p)
300			c += sizeof(char *);
301	c += sizeof(char *);	/* one extra for NULL term */
302
303	rbuf = xrealloc(rbuf, tot + c + align);	/* make room for ptrs AT END */
304	endbuf = rbuf + tot;	/* addr just past data buf */
305	q = ret = (char **)(endbuf + align);	/* ==> free(*ret) to dealloc */
306	*q++ = p = rbuf;	/* point ptrs to the strings */
307	endbuf--;		/* do not traverse final NUL */
308	while (++p < endbuf)
309		if (!*p)	/* NUL char implies that */
310			*q++ = p + 1;	/* next string -> next char */
311
312	*q = 0;			/* null ptr list terminator */
313	return ret;
314}
315
316// warning: interface may change
317int read_cmdline(char *restrict const dst, unsigned sz, unsigned pid)
318{
319	char name[32];
320	int fd;
321	unsigned n = 0;
322	dst[0] = '\0';
323	snprintf(name, sizeof name, "/proc/%u/cmdline", pid);
324	fd = open(name, O_RDONLY);
325	if (fd == -1)
326		return 0;
327	for (;;) {
328		ssize_t r = read(fd, dst + n, sz - n);
329		if (r == -1) {
330			if (errno == EINTR)
331				continue;
332			break;
333		}
334		n += r;
335		if (n == sz)
336			break;	// filled the buffer
337		if (r == 0)
338			break;	// EOF
339	}
340	if (n) {
341		int i;
342		if (n == sz)
343			n--;
344		dst[n] = '\0';
345		i = n;
346		while (i--) {
347			int c = dst[i];
348			if (c < ' ' || c > '~')
349				dst[i] = ' ';
350		}
351	}
352	return n;
353}
354
355/* These are some nice GNU C expression subscope "inline" functions.
356 * The can be used with arbitrary types and evaluate their arguments
357 * exactly once.
358 */
359
360/* Test if item X of type T is present in the 0 terminated list L */
361#define XinL(T, X, L) ( {			\
362	    T  x = (X), *l = (L);		\
363	    while (*l && *l != x) l++;		\
364	    *l == x;				\
365	} )
366
367/* Test if item X of type T is present in the list L of length N */
368#define XinLN(T, X, L, N) ( {		\
369	    T x = (X), *l = (L);		\
370	    int i = 0, n = (N);			\
371	    while (i < n && l[i] != x) i++;	\
372	    i < n && l[i] == x;			\
373	} )
374
375/* readproc: return a pointer to a proc_t filled with requested info about the
376 * next process available matching the restriction set.  If no more such
377 * processes are available, return a null pointer (boolean false).  Use the
378 * passed buffer instead of allocating space if it is non-NULL.  */
379
380/* This is optimized so that if a PID list is given, only those files are
381 * searched for in /proc.  If other lists are given in addition to the PID list,
382 * the same logic can follow through as for the no-PID list case.  This is
383 * fairly complex, but it does try to not to do any unnecessary work.
384 */
385proc_t *readproc(PROCTAB * PT, proc_t * p)
386{
387	static struct direct *ent;	/* dirent handle */
388	static struct stat sb;	/* stat buffer */
389	static char path[32], sbuf[1024];	/* bufs for stat,statm */
390#ifdef FLASK_LINUX
391	security_id_t secsid;
392#endif
393	pid_t pid;		// saved until we have a proc_t allocated for sure
394
395	/* loop until a proc matching restrictions is found or no more processes */
396	/* I know this could be a while loop -- this way is easier to indent ;-) */
397next_proc:			/* get next PID for consideration */
398
399/*printf("PT->flags is 0x%08x\n", PT->flags);*/
400#define flags (PT->flags)
401
402	if (flags & PROC_PID) {
403		pid = *(PT->pids)++;
404		if (unlikely(!pid))
405			return NULL;
406		snprintf(path, sizeof path, "/proc/%d", pid);
407	} else {		/* get next numeric /proc ent */
408		for (;;) {
409			ent = readdir(PT->procfs);
410			if (unlikely(unlikely(!ent) || unlikely(!ent->d_name)))
411				return NULL;
412			if (likely
413			    (likely(*ent->d_name > '0')
414			     && likely(*ent->d_name <= '9')))
415				break;
416		}
417		pid = strtoul(ent->d_name, NULL, 10);
418		memcpy(path, "/proc/", 6);
419		strcpy(path + 6, ent->d_name);	// trust /proc to not contain evil top-level entries
420//      snprintf(path, sizeof path, "/proc/%s", ent->d_name);
421	}
422#ifdef FLASK_LINUX
423	if (stat_secure(path, &sb, &secsid) == -1)	/* no such dirent (anymore) */
424#else
425	if (unlikely(stat(path, &sb) == -1))	/* no such dirent (anymore) */
426#endif
427		goto next_proc;
428
429	if ((flags & PROC_UID) && !XinLN(uid_t, sb.st_uid, PT->uids, PT->nuid))
430		goto next_proc;	/* not one of the requested uids */
431
432	if (!p)
433		p = xcalloc(p, sizeof *p);	/* passed buf or alloced mem */
434
435	p->euid = sb.st_uid;	/* need a way to get real uid */
436#ifdef FLASK_LINUX
437	p->secsid = secsid;
438#endif
439	p->pid = pid;
440
441	if (flags & PROC_FILLSTAT) {	/* read, parse /proc/#/stat */
442		if (unlikely(file2str(path, "stat", sbuf, sizeof sbuf) == -1))
443			goto next_proc;	/* error reading /proc/#/stat */
444		stat2proc(sbuf, p);	/* parse /proc/#/stat */
445	}
446
447	if (unlikely(flags & PROC_FILLMEM)) {	/* read, parse /proc/#/statm */
448		if (likely(file2str(path, "statm", sbuf, sizeof sbuf) != -1))
449			statm2proc(sbuf, p);	/* ignore statm errors here */
450	}
451	/* statm fields just zero */
452	if (flags & PROC_FILLSTATUS) {	/* read, parse /proc/#/status */
453		if (likely(file2str(path, "status", sbuf, sizeof sbuf) != -1)) {
454			status2proc(sbuf, p);
455		}
456	}
457
458	/* some number->text resolving which is time consuming */
459	if (flags & PROC_FILLUSR) {
460		strncpy(p->euser, user_from_uid(p->euid), sizeof p->euser);
461		if (flags & PROC_FILLSTATUS) {
462			strncpy(p->ruser, user_from_uid(p->ruid),
463				sizeof p->ruser);
464			strncpy(p->suser, user_from_uid(p->suid),
465				sizeof p->suser);
466			strncpy(p->fuser, user_from_uid(p->fuid),
467				sizeof p->fuser);
468		}
469	}
470
471	/* some number->text resolving which is time consuming */
472	if (flags & PROC_FILLGRP) {
473		strncpy(p->egroup, group_from_gid(p->egid), sizeof p->egroup);
474		if (flags & PROC_FILLSTATUS) {
475			strncpy(p->rgroup, group_from_gid(p->rgid),
476				sizeof p->rgroup);
477			strncpy(p->sgroup, group_from_gid(p->sgid),
478				sizeof p->sgroup);
479			strncpy(p->fgroup, group_from_gid(p->fgid),
480				sizeof p->fgroup);
481		}
482	}
483
484	if ((flags & PROC_FILLCOM) || (flags & PROC_FILLARG))	/* read+parse /proc/#/cmdline */
485		p->cmdline = file2strvec(path, "cmdline");
486	else
487		p->cmdline = NULL;
488
489	if (unlikely(flags & PROC_FILLENV))	/* read+parse /proc/#/environ */
490		p->environ = file2strvec(path, "environ");
491	else
492		p->environ = NULL;
493
494	return p;
495}
496
497#undef flags
498
499/* ps_readproc: return a pointer to a proc_t filled with requested info about the
500 * next process available matching the restriction set.  If no more such
501 * processes are available, return a null pointer (boolean false).  Use the
502 * passed buffer instead of allocating space if it is non-NULL.  */
503
504/* This is optimized so that if a PID list is given, only those files are
505 * searched for in /proc.  If other lists are given in addition to the PID list,
506 * the same logic can follow through as for the no-PID list case.  This is
507 * fairly complex, but it does try to not to do any unnecessary work.
508 */
509proc_t *ps_readproc(PROCTAB * PT, proc_t * p)
510{
511	static struct direct *ent;	/* dirent handle */
512	static struct stat sb;	/* stat buffer */
513	static char path[32], sbuf[1024];	/* bufs for stat,statm */
514#ifdef FLASK_LINUX
515	security_id_t secsid;
516#endif
517	pid_t pid;		// saved until we have a proc_t allocated for sure
518
519	/* loop until a proc matching restrictions is found or no more processes */
520	/* I know this could be a while loop -- this way is easier to indent ;-) */
521next_proc:			/* get next PID for consideration */
522
523/*printf("PT->flags is 0x%08x\n", PT->flags);*/
524#define flags (PT->flags)
525
526	for (;;) {
527		ent = readdir(PT->procfs);
528		if (unlikely(unlikely(!ent) || unlikely(!ent->d_name)))
529			return NULL;
530		if (likely
531		    (likely(*ent->d_name > '0') && likely(*ent->d_name <= '9')))
532			break;
533	}
534	pid = strtoul(ent->d_name, NULL, 10);
535	memcpy(path, "/proc/", 6);
536	strcpy(path + 6, ent->d_name);	// trust /proc to not contain evil top-level entries
537//  snprintf(path, sizeof path, "/proc/%s", ent->d_name);
538
539#ifdef FLASK_LINUX
540	if (stat_secure(path, &sb, &secsid) == -1)	/* no such dirent (anymore) */
541#else
542	if (stat(path, &sb) == -1)	/* no such dirent (anymore) */
543#endif
544		goto next_proc;
545
546	if (!p)
547		p = xcalloc(p, sizeof *p);	/* passed buf or alloced mem */
548
549	p->euid = sb.st_uid;	/* need a way to get real uid */
550#ifdef FLASK_LINUX
551	p->secsid = secsid;
552#endif
553	p->pid = pid;
554
555	if ((file2str(path, "stat", sbuf, sizeof sbuf)) == -1)
556		goto next_proc;	/* error reading /proc/#/stat */
557	stat2proc(sbuf, p);	/* parse /proc/#/stat */
558
559	if (flags & PROC_FILLMEM) {	/* read, parse /proc/#/statm */
560		if ((file2str(path, "statm", sbuf, sizeof sbuf)) != -1)
561			statm2proc(sbuf, p);	/* ignore statm errors here */
562	}
563
564	/* statm fields just zero */
565	/*  if (flags & PROC_FILLSTATUS) { */
566	/* read, parse /proc/#/status */
567	if ((file2str(path, "status", sbuf, sizeof sbuf)) != -1) {
568		status2proc(sbuf, p);
569	}
570/*    }*/
571
572	/* some number->text resolving which is time consuming */
573	if (flags & PROC_FILLUSR) {
574		strncpy(p->euser, user_from_uid(p->euid), sizeof p->euser);
575/*        if (flags & PROC_FILLSTATUS) { */
576		strncpy(p->ruser, user_from_uid(p->ruid), sizeof p->ruser);
577		strncpy(p->suser, user_from_uid(p->suid), sizeof p->suser);
578		strncpy(p->fuser, user_from_uid(p->fuid), sizeof p->fuser);
579/*        }*/
580	}
581
582	/* some number->text resolving which is time consuming */
583	if (flags & PROC_FILLGRP) {
584		strncpy(p->egroup, group_from_gid(p->egid), sizeof p->egroup);
585/*        if (flags & PROC_FILLSTATUS) { */
586		strncpy(p->rgroup, group_from_gid(p->rgid), sizeof p->rgroup);
587		strncpy(p->sgroup, group_from_gid(p->sgid), sizeof p->sgroup);
588		strncpy(p->fgroup, group_from_gid(p->fgid), sizeof p->fgroup);
589/*        }*/
590	}
591
592	if ((flags & PROC_FILLCOM) || (flags & PROC_FILLARG))	/* read+parse /proc/#/cmdline */
593		p->cmdline = file2strvec(path, "cmdline");
594	else
595		p->cmdline = NULL;
596
597	if (flags & PROC_FILLENV)	/* read+parse /proc/#/environ */
598		p->environ = file2strvec(path, "environ");
599	else
600		p->environ = NULL;
601
602	return p;
603}
604
605#undef flags
606
607void look_up_our_self(proc_t * p)
608{
609	static char path[32], sbuf[1024];	/* bufs for stat,statm */
610	sprintf(path, "/proc/%d", getpid());
611	file2str(path, "stat", sbuf, sizeof sbuf);
612	stat2proc(sbuf, p);	/* parse /proc/#/stat */
613	file2str(path, "statm", sbuf, sizeof sbuf);
614	statm2proc(sbuf, p);	/* ignore statm errors here */
615	file2str(path, "status", sbuf, sizeof sbuf);
616	status2proc(sbuf, p);
617}
618
619/* Convenient wrapper around openproc and readproc to slurp in the whole process
620 * table subset satisfying the constraints of flags and the optional PID list.
621 * Free allocated memory with freeproctab().  Access via tab[N]->member.  The
622 * pointer list is NULL terminated.
623 */
624proc_t **readproctab(int flags, ...)
625{
626	PROCTAB *PT = NULL;
627	proc_t **tab = NULL;
628	int n = 0;
629	va_list ap;
630
631	va_start(ap, flags);	/* pass through args to openproc */
632	if (flags & PROC_UID) {
633		/* temporary variables to ensure that va_arg() instances
634		 * are called in the right order
635		 */
636		uid_t *u;
637		int i;
638
639		u = va_arg(ap, uid_t *);
640		i = va_arg(ap, int);
641		PT = openproc(flags, u, i);
642	} else if (flags & PROC_PID)
643		PT = openproc(flags, va_arg(ap, void *));	/* assume ptr sizes same */
644	else
645		PT = openproc(flags);
646	va_end(ap);
647	do {			/* read table: */
648		tab = xrealloc(tab, (n + 1) * sizeof(proc_t *));	/* realloc as we go, using */
649		tab[n] = readproc(PT, NULL);	/* final null to terminate */
650	} while (tab[n++]);	/* stop when NULL reached */
651	closeproc(PT);
652	return tab;
653}
654