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