2018-07-06 05:10:41 +00:00
|
|
|
|
/* Run a test case in an isolated namespace.
|
|
|
|
|
Copyright (C) 2018 Free Software Foundation, Inc.
|
|
|
|
|
This file is part of the GNU C Library.
|
|
|
|
|
|
|
|
|
|
The GNU C Library is free software; you can redistribute it and/or
|
|
|
|
|
modify it under the terms of the GNU Lesser General Public
|
|
|
|
|
License as published by the Free Software Foundation; either
|
|
|
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
The GNU C Library is distributed in the hope that it will be useful,
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
|
Lesser General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
|
|
|
License along with the GNU C Library; if not, see
|
|
|
|
|
<http://www.gnu.org/licenses/>. */
|
|
|
|
|
|
|
|
|
|
#define _FILE_OFFSET_BITS 64
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <sched.h>
|
|
|
|
|
#include <sys/syscall.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <dirent.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <sys/fcntl.h>
|
|
|
|
|
#include <sys/file.h>
|
|
|
|
|
#include <sys/wait.h>
|
|
|
|
|
#include <stdarg.h>
|
|
|
|
|
#include <sys/sysmacros.h>
|
|
|
|
|
#include <ctype.h>
|
|
|
|
|
#include <utime.h>
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <error.h>
|
|
|
|
|
|
|
|
|
|
#ifdef __linux__
|
|
|
|
|
#include <sys/mount.h>
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include <support/support.h>
|
|
|
|
|
#include <support/xunistd.h>
|
|
|
|
|
#include "check.h"
|
|
|
|
|
#include "test-driver.h"
|
|
|
|
|
|
|
|
|
|
#ifndef __linux__
|
|
|
|
|
#define mount(s,t,fs,f,d) no_mount()
|
|
|
|
|
int no_mount (void)
|
|
|
|
|
{
|
|
|
|
|
FAIL_UNSUPPORTED("mount not supported; port needed");
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
int verbose = 0;
|
|
|
|
|
|
|
|
|
|
/* Running a test in a container is tricky. There are two main
|
|
|
|
|
categories of things to do:
|
|
|
|
|
|
|
|
|
|
1. "Once" actions, like setting up the container and doing an
|
|
|
|
|
install into it.
|
|
|
|
|
|
|
|
|
|
2. "Per-test" actions, like copying in support files and
|
|
|
|
|
configuring the container.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"Once" actions:
|
|
|
|
|
|
|
|
|
|
* mkdir $buildroot/testroot.pristine/
|
|
|
|
|
* install into it
|
|
|
|
|
* rsync to $buildroot/testroot.root/
|
|
|
|
|
|
|
|
|
|
"Per-test" actions:
|
|
|
|
|
* maybe rsync to $buildroot/testroot.root/
|
|
|
|
|
* copy support files and test binary
|
|
|
|
|
* chroot/unshare
|
|
|
|
|
* set up any mounts (like /proc)
|
|
|
|
|
|
|
|
|
|
Magic files:
|
|
|
|
|
|
|
|
|
|
For test $srcdir/foo/mytest.c we look for $srcdir/foo/mytest.root
|
|
|
|
|
and, if found...
|
|
|
|
|
|
|
|
|
|
* mytest.root/ is rsync'd into container
|
|
|
|
|
* mytest.root/preclean.req causes fresh rsync (with delete) before
|
|
|
|
|
test if present
|
|
|
|
|
* mytest.root/mytset.script has a list of "commands" to run:
|
|
|
|
|
syntax:
|
|
|
|
|
# comment
|
|
|
|
|
mv FILE FILE
|
|
|
|
|
cp FILE FILE
|
|
|
|
|
rm FILE
|
|
|
|
|
FILE must start with $B/, $S/, $I/, $L/, or /
|
|
|
|
|
(expands to build dir, source dir, install dir, library dir
|
|
|
|
|
(in container), or container's root)
|
|
|
|
|
* mytest.root/postclean.req causes fresh rsync (with delete) after
|
|
|
|
|
test if present
|
|
|
|
|
|
|
|
|
|
Note that $srcdir/foo/mytest.script may be used instead of a
|
|
|
|
|
$srcdir/foo/mytest.root/mytest.script in the sysroot template, if
|
|
|
|
|
there is no other reason for a sysroot.
|
|
|
|
|
|
|
|
|
|
Design goals:
|
|
|
|
|
|
|
|
|
|
* independent of other packages which may not be installed (like
|
|
|
|
|
rsync or Docker, or even "cp")
|
|
|
|
|
|
|
|
|
|
* Simple, easy to review code (i.e. prefer simple naive code over
|
|
|
|
|
complex efficient code)
|
|
|
|
|
|
|
|
|
|
* The current implementation ist parallel-make-safe, but only in
|
|
|
|
|
that it uses a lock to prevent parallel access to the testroot. */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Utility Functions */
|
|
|
|
|
|
|
|
|
|
/* Like xunlink, but it's OK if the file already doesn't exist. */
|
|
|
|
|
void
|
|
|
|
|
maybe_xunlink (const char *path)
|
|
|
|
|
{
|
|
|
|
|
int rv = unlink (path);
|
|
|
|
|
if (rv < 0 && errno != ENOENT)
|
|
|
|
|
FAIL_EXIT1 ("unlink (\"%s\"): %m", path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Like xmkdir, but it's OK if the directory already exists. */
|
|
|
|
|
void
|
|
|
|
|
maybe_xmkdir (const char *path, mode_t mode)
|
|
|
|
|
{
|
|
|
|
|
struct stat st;
|
|
|
|
|
|
|
|
|
|
if (stat (path, &st) == 0
|
|
|
|
|
&& S_ISDIR (st.st_mode))
|
|
|
|
|
return;
|
|
|
|
|
xmkdir (path, mode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Temporarily concatenate multiple strings into one. Allows up to 10
|
|
|
|
|
temporary results; use strdup () if you need them to be
|
|
|
|
|
permanent. */
|
|
|
|
|
static char *
|
|
|
|
|
concat (const char *str, ...)
|
|
|
|
|
{
|
|
|
|
|
/* Assume initialized to NULL/zero. */
|
|
|
|
|
static char *bufs[10];
|
|
|
|
|
static size_t buflens[10];
|
|
|
|
|
static int bufn = 0;
|
|
|
|
|
int n;
|
|
|
|
|
size_t len;
|
|
|
|
|
va_list ap, ap2;
|
|
|
|
|
char *cp;
|
|
|
|
|
char *next;
|
|
|
|
|
|
|
|
|
|
va_start (ap, str);
|
|
|
|
|
va_copy (ap2, ap);
|
|
|
|
|
|
|
|
|
|
n = bufn;
|
|
|
|
|
bufn = (bufn + 1) % 10;
|
|
|
|
|
len = strlen (str);
|
|
|
|
|
|
|
|
|
|
while ((next = va_arg (ap, char *)) != NULL)
|
|
|
|
|
len = len + strlen (next);
|
|
|
|
|
|
|
|
|
|
va_end (ap);
|
|
|
|
|
|
|
|
|
|
if (bufs[n] == NULL)
|
|
|
|
|
{
|
|
|
|
|
bufs[n] = xmalloc (len + 1); /* NUL */
|
|
|
|
|
buflens[n] = len + 1;
|
|
|
|
|
}
|
|
|
|
|
else if (buflens[n] < len + 1)
|
|
|
|
|
{
|
|
|
|
|
bufs[n] = xrealloc (bufs[n], len + 1); /* NUL */
|
|
|
|
|
buflens[n] = len + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
strcpy (bufs[n], str);
|
|
|
|
|
cp = strchr (bufs[n], '\0');
|
|
|
|
|
while ((next = va_arg (ap2, char *)) != NULL)
|
|
|
|
|
{
|
|
|
|
|
strcpy (cp, next);
|
|
|
|
|
cp = strchr (cp, '\0');
|
|
|
|
|
}
|
|
|
|
|
*cp = 0;
|
|
|
|
|
va_end (ap2);
|
|
|
|
|
|
|
|
|
|
return bufs[n];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Try to mount SRC onto DEST. */
|
|
|
|
|
static void
|
|
|
|
|
trymount (const char *src, const char *dest)
|
|
|
|
|
{
|
|
|
|
|
if (mount (src, dest, "", MS_BIND, NULL) < 0)
|
|
|
|
|
FAIL_EXIT1 ("can't mount %s onto %s\n", src, dest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Special case of above for devices like /dev/zero where we have to
|
|
|
|
|
mount a device over a device, not a directory over a directory. */
|
|
|
|
|
static void
|
|
|
|
|
devmount (const char *new_root_path, const char *which)
|
|
|
|
|
{
|
|
|
|
|
int fd;
|
|
|
|
|
fd = open (concat (new_root_path, "/dev/", which, NULL),
|
|
|
|
|
O_CREAT | O_TRUNC | O_RDWR, 0777);
|
|
|
|
|
xclose (fd);
|
|
|
|
|
|
|
|
|
|
trymount (concat ("/dev/", which, NULL),
|
|
|
|
|
concat (new_root_path, "/dev/", which, NULL));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Returns true if the string "looks like" an environement variable
|
|
|
|
|
being set. */
|
|
|
|
|
static int
|
|
|
|
|
is_env_setting (const char *a)
|
|
|
|
|
{
|
|
|
|
|
int count_name = 0;
|
|
|
|
|
|
|
|
|
|
while (*a)
|
|
|
|
|
{
|
|
|
|
|
if (isalnum (*a) || *a == '_')
|
|
|
|
|
++count_name;
|
|
|
|
|
else if (*a == '=' && count_name > 0)
|
|
|
|
|
return 1;
|
|
|
|
|
else
|
|
|
|
|
return 0;
|
|
|
|
|
++a;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Break the_line into words and store in the_words. Max nwords,
|
|
|
|
|
returns actual count. */
|
|
|
|
|
static int
|
|
|
|
|
tokenize (char *the_line, char **the_words, int nwords)
|
|
|
|
|
{
|
|
|
|
|
int rv = 0;
|
|
|
|
|
|
|
|
|
|
while (nwords > 0)
|
|
|
|
|
{
|
|
|
|
|
/* Skip leading whitespace, if any. */
|
|
|
|
|
while (*the_line && isspace (*the_line))
|
|
|
|
|
++the_line;
|
|
|
|
|
|
|
|
|
|
/* End of line? */
|
|
|
|
|
if (*the_line == 0)
|
|
|
|
|
return rv;
|
|
|
|
|
|
|
|
|
|
/* THE_LINE points to a non-whitespace character, so we have a
|
|
|
|
|
word. */
|
|
|
|
|
*the_words = the_line;
|
|
|
|
|
++the_words;
|
|
|
|
|
nwords--;
|
|
|
|
|
++rv;
|
|
|
|
|
|
|
|
|
|
/* Skip leading whitespace, if any. */
|
|
|
|
|
while (*the_line && ! isspace (*the_line))
|
|
|
|
|
++the_line;
|
|
|
|
|
|
|
|
|
|
/* We now point at the trailing NUL *or* some whitespace. */
|
|
|
|
|
if (*the_line == 0)
|
|
|
|
|
return rv;
|
|
|
|
|
|
|
|
|
|
/* It was whitespace, skip and keep tokenizing. */
|
|
|
|
|
*the_line++ = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* We get here if we filled the words buffer. */
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Mini-RSYNC implementation. Optimize later. */
|
|
|
|
|
|
|
|
|
|
/* A few routines for an "rsync buffer" which stores the paths we're
|
|
|
|
|
working on. We continuously grow and shrink the paths in each
|
|
|
|
|
buffer so there's lot of re-use. */
|
|
|
|
|
|
|
|
|
|
/* We rely on "initialized to zero" to set these up. */
|
|
|
|
|
typedef struct
|
|
|
|
|
{
|
|
|
|
|
char *buf;
|
|
|
|
|
size_t len;
|
|
|
|
|
size_t size;
|
|
|
|
|
} path_buf;
|
|
|
|
|
|
|
|
|
|
static path_buf spath, dpath;
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
r_setup (char *path, path_buf * pb)
|
|
|
|
|
{
|
|
|
|
|
size_t len = strlen (path);
|
|
|
|
|
if (pb->buf == NULL || pb->size < len + 1)
|
|
|
|
|
{
|
|
|
|
|
/* Round up. This is an arbitrary number, just to keep from
|
|
|
|
|
reallocing too often. */
|
|
|
|
|
size_t sz = ALIGN_UP (len + 1, 512);
|
|
|
|
|
if (pb->buf == NULL)
|
|
|
|
|
pb->buf = (char *) xmalloc (sz);
|
|
|
|
|
else
|
|
|
|
|
pb->buf = (char *) xrealloc (pb->buf, sz);
|
|
|
|
|
if (pb->buf == NULL)
|
|
|
|
|
FAIL_EXIT1 ("Out of memory while rsyncing\n");
|
|
|
|
|
|
|
|
|
|
pb->size = sz;
|
|
|
|
|
}
|
|
|
|
|
strcpy (pb->buf, path);
|
|
|
|
|
pb->len = len;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
r_append (const char *path, path_buf * pb)
|
|
|
|
|
{
|
|
|
|
|
size_t len = strlen (path) + pb->len;
|
|
|
|
|
if (pb->size < len + 1)
|
|
|
|
|
{
|
|
|
|
|
/* Round up */
|
|
|
|
|
size_t sz = ALIGN_UP (len + 1, 512);
|
|
|
|
|
pb->buf = (char *) xrealloc (pb->buf, sz);
|
|
|
|
|
if (pb->buf == NULL)
|
|
|
|
|
FAIL_EXIT1 ("Out of memory while rsyncing\n");
|
|
|
|
|
|
|
|
|
|
pb->size = sz;
|
|
|
|
|
}
|
|
|
|
|
strcpy (pb->buf + pb->len, path);
|
|
|
|
|
pb->len = len;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
|
file_exists (char *path)
|
|
|
|
|
{
|
|
|
|
|
struct stat st;
|
|
|
|
|
if (lstat (path, &st) == 0)
|
|
|
|
|
return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
recursive_remove (char *path)
|
|
|
|
|
{
|
|
|
|
|
pid_t child;
|
|
|
|
|
int status;
|
|
|
|
|
|
|
|
|
|
child = fork ();
|
|
|
|
|
|
|
|
|
|
switch (child) {
|
|
|
|
|
case -1:
|
|
|
|
|
FAIL_EXIT1 ("Unable to fork");
|
|
|
|
|
case 0:
|
|
|
|
|
/* Child. */
|
|
|
|
|
execlp ("rm", "rm", "-rf", path, NULL);
|
|
|
|
|
default:
|
|
|
|
|
/* Parent. */
|
|
|
|
|
waitpid (child, &status, 0);
|
|
|
|
|
/* "rm" would have already printed a suitable error message. */
|
|
|
|
|
if (! WIFEXITED (status)
|
|
|
|
|
|| WEXITSTATUS (status) != 0)
|
|
|
|
|
exit (1);
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Used for both rsync and the mytest.script "cp" command. */
|
|
|
|
|
static void
|
|
|
|
|
copy_one_file (const char *sname, const char *dname)
|
|
|
|
|
{
|
|
|
|
|
int sfd, dfd;
|
|
|
|
|
struct stat st;
|
|
|
|
|
struct utimbuf times;
|
|
|
|
|
|
|
|
|
|
sfd = open (sname, O_RDONLY);
|
|
|
|
|
if (sfd < 0)
|
|
|
|
|
FAIL_EXIT1 ("unable to open %s for reading\n", sname);
|
|
|
|
|
|
|
|
|
|
if (fstat (sfd, &st) < 0)
|
|
|
|
|
FAIL_EXIT1 ("unable to fstat %s\n", sname);
|
|
|
|
|
|
|
|
|
|
dfd = open (dname, O_WRONLY | O_TRUNC | O_CREAT, 0600);
|
|
|
|
|
if (dfd < 0)
|
|
|
|
|
FAIL_EXIT1 ("unable to open %s for writing\n", dname);
|
|
|
|
|
|
2018-08-31 20:07:47 +00:00
|
|
|
|
xcopy_file_range (sfd, 0, dfd, 0, st.st_size, 0);
|
2018-07-06 05:10:41 +00:00
|
|
|
|
|
|
|
|
|
xclose (sfd);
|
|
|
|
|
xclose (dfd);
|
|
|
|
|
|
|
|
|
|
if (chmod (dname, st.st_mode & 0777) < 0)
|
|
|
|
|
FAIL_EXIT1 ("chmod %s: %s\n", dname, strerror (errno));
|
|
|
|
|
|
|
|
|
|
times.actime = st.st_atime;
|
|
|
|
|
times.modtime = st.st_mtime;
|
|
|
|
|
if (utime (dname, ×) < 0)
|
|
|
|
|
FAIL_EXIT1 ("utime %s: %s\n", dname, strerror (errno));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* We don't check *everything* about the two files to see if a copy is
|
|
|
|
|
needed, just the minimum to make sure we get the latest copy. */
|
|
|
|
|
static int
|
|
|
|
|
need_sync (char *ap, char *bp, struct stat *a, struct stat *b)
|
|
|
|
|
{
|
|
|
|
|
if ((a->st_mode & S_IFMT) != (b->st_mode & S_IFMT))
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
|
|
if (S_ISLNK (a->st_mode))
|
|
|
|
|
{
|
|
|
|
|
int rv;
|
|
|
|
|
char *al, *bl;
|
|
|
|
|
|
|
|
|
|
if (a->st_size != b->st_size)
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
|
|
al = xreadlink (ap);
|
|
|
|
|
bl = xreadlink (bp);
|
|
|
|
|
rv = strcmp (al, bl);
|
|
|
|
|
free (al);
|
|
|
|
|
free (bl);
|
|
|
|
|
if (rv == 0)
|
|
|
|
|
return 0; /* links are same */
|
|
|
|
|
return 1; /* links differ */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (verbose)
|
|
|
|
|
{
|
|
|
|
|
if (a->st_size != b->st_size)
|
|
|
|
|
printf ("SIZE\n");
|
|
|
|
|
if ((a->st_mode & 0777) != (b->st_mode & 0777))
|
|
|
|
|
printf ("MODE\n");
|
|
|
|
|
if (a->st_mtime != b->st_mtime)
|
|
|
|
|
printf ("TIME\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (a->st_size == b->st_size
|
|
|
|
|
&& ((a->st_mode & 0777) == (b->st_mode & 0777))
|
|
|
|
|
&& a->st_mtime == b->st_mtime)
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
rsync_1 (path_buf * src, path_buf * dest, int and_delete)
|
|
|
|
|
{
|
|
|
|
|
DIR *dir;
|
|
|
|
|
struct dirent *de;
|
|
|
|
|
struct stat s, d;
|
|
|
|
|
|
|
|
|
|
r_append ("/", src);
|
|
|
|
|
r_append ("/", dest);
|
|
|
|
|
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("sync %s to %s %s\n", src->buf, dest->buf,
|
|
|
|
|
and_delete ? "and delete" : "");
|
|
|
|
|
|
|
|
|
|
size_t staillen = src->len;
|
|
|
|
|
|
|
|
|
|
size_t dtaillen = dest->len;
|
|
|
|
|
|
|
|
|
|
dir = opendir (src->buf);
|
|
|
|
|
|
|
|
|
|
while ((de = readdir (dir)) != NULL)
|
|
|
|
|
{
|
|
|
|
|
if (strcmp (de->d_name, ".") == 0
|
|
|
|
|
|| strcmp (de->d_name, "..") == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
src->len = staillen;
|
|
|
|
|
r_append (de->d_name, src);
|
|
|
|
|
dest->len = dtaillen;
|
|
|
|
|
r_append (de->d_name, dest);
|
|
|
|
|
|
|
|
|
|
s.st_mode = ~0;
|
|
|
|
|
d.st_mode = ~0;
|
|
|
|
|
|
|
|
|
|
if (lstat (src->buf, &s) != 0)
|
|
|
|
|
FAIL_EXIT1 ("%s obtained by readdir, but stat failed.\n", src->buf);
|
|
|
|
|
|
|
|
|
|
/* It's OK if this one fails, since we know the file might be
|
|
|
|
|
missing. */
|
|
|
|
|
lstat (dest->buf, &d);
|
|
|
|
|
|
|
|
|
|
if (! need_sync (src->buf, dest->buf, &s, &d))
|
|
|
|
|
{
|
|
|
|
|
if (S_ISDIR (s.st_mode))
|
|
|
|
|
rsync_1 (src, dest, and_delete);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (d.st_mode != ~0)
|
|
|
|
|
switch (d.st_mode & S_IFMT)
|
|
|
|
|
{
|
|
|
|
|
case S_IFDIR:
|
|
|
|
|
if (!S_ISDIR (s.st_mode))
|
|
|
|
|
{
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("-D %s\n", dest->buf);
|
|
|
|
|
recursive_remove (dest->buf);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("-F %s\n", dest->buf);
|
|
|
|
|
maybe_xunlink (dest->buf);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (s.st_mode & S_IFMT)
|
|
|
|
|
{
|
|
|
|
|
case S_IFREG:
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("+F %s\n", dest->buf);
|
|
|
|
|
copy_one_file (src->buf, dest->buf);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case S_IFDIR:
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("+D %s\n", dest->buf);
|
|
|
|
|
maybe_xmkdir (dest->buf, (s.st_mode & 0777) | 0700);
|
|
|
|
|
rsync_1 (src, dest, and_delete);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case S_IFLNK:
|
|
|
|
|
{
|
|
|
|
|
char *lp;
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("+L %s\n", dest->buf);
|
|
|
|
|
lp = xreadlink (src->buf);
|
|
|
|
|
xsymlink (lp, dest->buf);
|
|
|
|
|
free (lp);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closedir (dir);
|
|
|
|
|
src->len = staillen;
|
|
|
|
|
src->buf[staillen] = 0;
|
|
|
|
|
dest->len = dtaillen;
|
|
|
|
|
dest->buf[dtaillen] = 0;
|
|
|
|
|
|
|
|
|
|
if (!and_delete)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
/* The rest of this function removes any files/directories in DEST
|
|
|
|
|
that do not exist in SRC. This is triggered as part of a
|
|
|
|
|
preclean or postsclean step. */
|
|
|
|
|
|
|
|
|
|
dir = opendir (dest->buf);
|
|
|
|
|
|
|
|
|
|
while ((de = readdir (dir)) != NULL)
|
|
|
|
|
{
|
|
|
|
|
if (strcmp (de->d_name, ".") == 0
|
|
|
|
|
|| strcmp (de->d_name, "..") == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
src->len = staillen;
|
|
|
|
|
r_append (de->d_name, src);
|
|
|
|
|
dest->len = dtaillen;
|
|
|
|
|
r_append (de->d_name, dest);
|
|
|
|
|
|
|
|
|
|
s.st_mode = ~0;
|
|
|
|
|
d.st_mode = ~0;
|
|
|
|
|
|
|
|
|
|
lstat (src->buf, &s);
|
|
|
|
|
|
|
|
|
|
if (lstat (dest->buf, &d) != 0)
|
|
|
|
|
FAIL_EXIT1 ("%s obtained by readdir, but stat failed.\n", dest->buf);
|
|
|
|
|
|
|
|
|
|
if (s.st_mode == ~0)
|
|
|
|
|
{
|
|
|
|
|
/* dest exists and src doesn't, clean it. */
|
|
|
|
|
switch (d.st_mode & S_IFMT)
|
|
|
|
|
{
|
|
|
|
|
case S_IFDIR:
|
|
|
|
|
if (!S_ISDIR (s.st_mode))
|
|
|
|
|
{
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("-D %s\n", dest->buf);
|
|
|
|
|
recursive_remove (dest->buf);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
if (verbose)
|
|
|
|
|
printf ("-F %s\n", dest->buf);
|
|
|
|
|
maybe_xunlink (dest->buf);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closedir (dir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
rsync (char *src, char *dest, int and_delete)
|
|
|
|
|
{
|
|
|
|
|
r_setup (src, &spath);
|
|
|
|
|
r_setup (dest, &dpath);
|
|
|
|
|
|
|
|
|
|
rsync_1 (&spath, &dpath, and_delete);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int
|
|
|
|
|
main (int argc, char **argv)
|
|
|
|
|
{
|
|
|
|
|
pid_t child;
|
|
|
|
|
char *pristine_root_path;
|
|
|
|
|
char *new_root_path;
|
|
|
|
|
char *new_cwd_path;
|
|
|
|
|
char *new_objdir_path;
|
|
|
|
|
char *new_srcdir_path;
|
|
|
|
|
char **new_child_proc;
|
|
|
|
|
char *command_root;
|
|
|
|
|
char *command_base;
|
|
|
|
|
char *command_basename;
|
|
|
|
|
char *so_base;
|
|
|
|
|
int do_postclean = 0;
|
|
|
|
|
|
|
|
|
|
uid_t original_uid;
|
|
|
|
|
gid_t original_gid;
|
|
|
|
|
int UMAP;
|
|
|
|
|
int GMAP;
|
|
|
|
|
/* Used for "%lld %lld 1" so need not be large. */
|
|
|
|
|
char tmp[100];
|
|
|
|
|
struct stat st;
|
|
|
|
|
int lock_fd;
|
|
|
|
|
|
|
|
|
|
setbuf (stdout, NULL);
|
|
|
|
|
|
|
|
|
|
/* The command line we're expecting looks like this:
|
|
|
|
|
env <set some vars> ld.so <library path> test-binary
|
|
|
|
|
|
|
|
|
|
We need to peel off any "env" or "ld.so" portion of the command
|
|
|
|
|
line, and keep track of which env vars we should preserve and
|
|
|
|
|
which we drop. */
|
|
|
|
|
|
|
|
|
|
if (argc < 2)
|
|
|
|
|
{
|
|
|
|
|
fprintf (stderr, "Usage: containerize <program to run> <args...>\n");
|
|
|
|
|
exit (1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (strcmp (argv[1], "-v") == 0)
|
|
|
|
|
{
|
|
|
|
|
verbose = 1;
|
|
|
|
|
++argv;
|
|
|
|
|
--argc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (strcmp (argv[1], "env") == 0)
|
|
|
|
|
{
|
|
|
|
|
++argv;
|
|
|
|
|
--argc;
|
|
|
|
|
while (is_env_setting (argv[1]))
|
|
|
|
|
{
|
|
|
|
|
/* If there are variables we do NOT want to propogate, this
|
|
|
|
|
is where the test for them goes. */
|
|
|
|
|
{
|
|
|
|
|
/* Need to keep these. Note that putenv stores a
|
|
|
|
|
pointer to our argv. */
|
|
|
|
|
putenv (argv[1]);
|
|
|
|
|
}
|
|
|
|
|
++argv;
|
|
|
|
|
--argc;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-04 11:07:29 +00:00
|
|
|
|
if (strcmp (argv[1], support_objdir_elf_ldso) == 0)
|
2018-07-06 05:10:41 +00:00
|
|
|
|
{
|
|
|
|
|
++argv;
|
|
|
|
|
--argc;
|
|
|
|
|
while (argv[1][0] == '-')
|
|
|
|
|
{
|
|
|
|
|
if (strcmp (argv[1], "--library-path") == 0)
|
|
|
|
|
{
|
|
|
|
|
++argv;
|
|
|
|
|
--argc;
|
|
|
|
|
}
|
|
|
|
|
++argv;
|
|
|
|
|
--argc;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pristine_root_path = strdup (concat (support_objdir_root,
|
|
|
|
|
"/testroot.pristine", NULL));
|
|
|
|
|
new_root_path = strdup (concat (support_objdir_root,
|
|
|
|
|
"/testroot.root", NULL));
|
|
|
|
|
new_cwd_path = get_current_dir_name ();
|
|
|
|
|
new_child_proc = argv + 1;
|
|
|
|
|
|
|
|
|
|
lock_fd = open (concat (pristine_root_path, "/lock.fd", NULL),
|
|
|
|
|
O_CREAT | O_TRUNC | O_RDWR, 0666);
|
|
|
|
|
if (lock_fd < 0)
|
|
|
|
|
FAIL_EXIT1 ("Cannot create testroot lock.\n");
|
|
|
|
|
|
|
|
|
|
while (flock (lock_fd, LOCK_EX) != 0)
|
|
|
|
|
{
|
|
|
|
|
if (errno != EINTR)
|
|
|
|
|
FAIL_EXIT1 ("Cannot lock testroot.\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xmkdirp (new_root_path, 0755);
|
|
|
|
|
|
|
|
|
|
/* We look for extra setup info in a subdir in the same spot as the
|
|
|
|
|
test, with the same name but a ".root" extension. This is that
|
|
|
|
|
directory. We try to look in the source tree if the path we're
|
|
|
|
|
given refers to the build tree, but we rely on the path to be
|
|
|
|
|
absolute. This is what the glibc makefiles do. */
|
|
|
|
|
command_root = concat (argv[1], ".root", NULL);
|
|
|
|
|
if (strncmp (command_root, support_objdir_root,
|
|
|
|
|
strlen (support_objdir_root)) == 0
|
|
|
|
|
&& command_root[strlen (support_objdir_root)] == '/')
|
|
|
|
|
command_root = concat (support_srcdir_root,
|
|
|
|
|
argv[1] + strlen (support_objdir_root),
|
|
|
|
|
".root", NULL);
|
|
|
|
|
command_root = strdup (command_root);
|
|
|
|
|
|
|
|
|
|
/* This cuts off the ".root" we appended above. */
|
|
|
|
|
command_base = strdup (command_root);
|
|
|
|
|
command_base[strlen (command_base) - 5] = 0;
|
|
|
|
|
|
|
|
|
|
/* This is the basename of the test we're running. */
|
|
|
|
|
command_basename = strrchr (command_base, '/');
|
|
|
|
|
if (command_basename == NULL)
|
|
|
|
|
command_basename = command_base;
|
|
|
|
|
else
|
|
|
|
|
++command_basename;
|
|
|
|
|
|
|
|
|
|
/* Shared object base directory. */
|
|
|
|
|
so_base = strdup (argv[1]);
|
|
|
|
|
if (strrchr (so_base, '/') != NULL)
|
|
|
|
|
strrchr (so_base, '/')[1] = 0;
|
|
|
|
|
|
|
|
|
|
if (file_exists (concat (command_root, "/postclean.req", NULL)))
|
|
|
|
|
do_postclean = 1;
|
|
|
|
|
|
|
|
|
|
rsync (pristine_root_path, new_root_path,
|
|
|
|
|
file_exists (concat (command_root, "/preclean.req", NULL)));
|
|
|
|
|
|
|
|
|
|
if (stat (command_root, &st) >= 0
|
|
|
|
|
&& S_ISDIR (st.st_mode))
|
|
|
|
|
rsync (command_root, new_root_path, 0);
|
|
|
|
|
|
|
|
|
|
new_objdir_path = strdup (concat (new_root_path,
|
|
|
|
|
support_objdir_root, NULL));
|
|
|
|
|
new_srcdir_path = strdup (concat (new_root_path,
|
|
|
|
|
support_srcdir_root, NULL));
|
|
|
|
|
|
|
|
|
|
/* new_cwd_path starts with '/' so no "/" needed between the two. */
|
|
|
|
|
xmkdirp (concat (new_root_path, new_cwd_path, NULL), 0755);
|
|
|
|
|
xmkdirp (new_srcdir_path, 0755);
|
|
|
|
|
xmkdirp (new_objdir_path, 0755);
|
|
|
|
|
|
|
|
|
|
original_uid = getuid ();
|
|
|
|
|
original_gid = getgid ();
|
|
|
|
|
|
|
|
|
|
/* Handle the cp/mv/rm "script" here. */
|
|
|
|
|
{
|
|
|
|
|
char *the_line = NULL;
|
|
|
|
|
size_t line_len = 0;
|
|
|
|
|
char *fname = concat (command_root, "/",
|
|
|
|
|
command_basename, ".script", NULL);
|
|
|
|
|
char *the_words[3];
|
|
|
|
|
FILE *f = fopen (fname, "r");
|
|
|
|
|
|
|
|
|
|
if (verbose && f)
|
|
|
|
|
fprintf (stderr, "running %s\n", fname);
|
|
|
|
|
|
|
|
|
|
if (f == NULL)
|
|
|
|
|
{
|
|
|
|
|
/* Try foo.script instead of foo.root/foo.script, as a shortcut. */
|
|
|
|
|
fname = concat (command_base, ".script", NULL);
|
|
|
|
|
f = fopen (fname, "r");
|
|
|
|
|
if (verbose && f)
|
|
|
|
|
fprintf (stderr, "running %s\n", fname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Note that we do NOT look for a Makefile-generated foo.script in
|
|
|
|
|
the build directory. If that is ever needed, this is the place
|
|
|
|
|
to add it. */
|
|
|
|
|
|
|
|
|
|
/* This is where we "interpret" the mini-script which is <test>.script. */
|
|
|
|
|
if (f != NULL)
|
|
|
|
|
{
|
|
|
|
|
while (getline (&the_line, &line_len, f) > 0)
|
|
|
|
|
{
|
|
|
|
|
int nt = tokenize (the_line, the_words, 3);
|
|
|
|
|
int i;
|
|
|
|
|
|
|
|
|
|
for (i = 1; i < nt; ++i)
|
|
|
|
|
{
|
|
|
|
|
if (memcmp (the_words[i], "$B/", 3) == 0)
|
|
|
|
|
the_words[i] = concat (support_objdir_root,
|
|
|
|
|
the_words[i] + 2, NULL);
|
|
|
|
|
else if (memcmp (the_words[i], "$S/", 3) == 0)
|
|
|
|
|
the_words[i] = concat (support_srcdir_root,
|
|
|
|
|
the_words[i] + 2, NULL);
|
|
|
|
|
else if (memcmp (the_words[i], "$I/", 3) == 0)
|
|
|
|
|
the_words[i] = concat (new_root_path,
|
|
|
|
|
support_install_prefix,
|
|
|
|
|
the_words[i] + 2, NULL);
|
|
|
|
|
else if (memcmp (the_words[i], "$L/", 3) == 0)
|
|
|
|
|
the_words[i] = concat (new_root_path,
|
|
|
|
|
support_libdir_prefix,
|
|
|
|
|
the_words[i] + 2, NULL);
|
|
|
|
|
else if (the_words[i][0] == '/')
|
|
|
|
|
the_words[i] = concat (new_root_path,
|
|
|
|
|
the_words[i], NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nt == 3 && the_words[2][strlen (the_words[2]) - 1] == '/')
|
|
|
|
|
{
|
|
|
|
|
char *r = strrchr (the_words[1], '/');
|
|
|
|
|
if (r)
|
|
|
|
|
the_words[2] = concat (the_words[2], r + 1, NULL);
|
|
|
|
|
else
|
|
|
|
|
the_words[2] = concat (the_words[2], the_words[1], NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nt == 2 && strcmp (the_words[0], "so") == 0)
|
|
|
|
|
{
|
|
|
|
|
the_words[2] = concat (new_root_path, support_libdir_prefix,
|
|
|
|
|
"/", the_words[1], NULL);
|
|
|
|
|
the_words[1] = concat (so_base, the_words[1], NULL);
|
|
|
|
|
copy_one_file (the_words[1], the_words[2]);
|
|
|
|
|
}
|
|
|
|
|
else if (nt == 3 && strcmp (the_words[0], "cp") == 0)
|
|
|
|
|
{
|
|
|
|
|
copy_one_file (the_words[1], the_words[2]);
|
|
|
|
|
}
|
|
|
|
|
else if (nt == 3 && strcmp (the_words[0], "mv") == 0)
|
|
|
|
|
{
|
|
|
|
|
if (rename (the_words[1], the_words[2]) < 0)
|
|
|
|
|
FAIL_EXIT1 ("rename %s -> %s: %s", the_words[1],
|
|
|
|
|
the_words[2], strerror (errno));
|
|
|
|
|
}
|
|
|
|
|
else if (nt == 3 && strcmp (the_words[0], "chmod") == 0)
|
|
|
|
|
{
|
|
|
|
|
long int m;
|
|
|
|
|
m = strtol (the_words[1], NULL, 0);
|
|
|
|
|
if (chmod (the_words[2], m) < 0)
|
|
|
|
|
FAIL_EXIT1 ("chmod %s: %s\n",
|
|
|
|
|
the_words[2], strerror (errno));
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
else if (nt == 2 && strcmp (the_words[0], "rm") == 0)
|
|
|
|
|
{
|
|
|
|
|
maybe_xunlink (the_words[1]);
|
|
|
|
|
}
|
|
|
|
|
else if (nt > 0 && the_words[0][0] != '#')
|
|
|
|
|
{
|
|
|
|
|
printf ("\033[31minvalid [%s]\033[0m\n", the_words[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fclose (f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef CLONE_NEWNS
|
|
|
|
|
/* The unshare here gives us our own spaces and capabilities. */
|
|
|
|
|
if (unshare (CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS) < 0)
|
|
|
|
|
{
|
2018-08-28 12:02:47 +00:00
|
|
|
|
/* Older kernels may not support all the options, or security
|
|
|
|
|
policy may block this call. */
|
|
|
|
|
if (errno == EINVAL || errno == EPERM)
|
2018-07-06 05:10:41 +00:00
|
|
|
|
FAIL_UNSUPPORTED ("unable to unshare user/fs: %s", strerror (errno));
|
|
|
|
|
else
|
|
|
|
|
FAIL_EXIT1 ("unable to unshare user/fs: %s", strerror (errno));
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
/* Some targets may not support unshare at all. */
|
|
|
|
|
FAIL_UNSUPPORTED ("unshare support missing");
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
/* Some systems, by default, all mounts leak out of the namespace. */
|
|
|
|
|
if (mount ("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0)
|
|
|
|
|
FAIL_EXIT1 ("could not create a private mount namespace\n");
|
|
|
|
|
|
|
|
|
|
trymount (support_srcdir_root, new_srcdir_path);
|
|
|
|
|
trymount (support_objdir_root, new_objdir_path);
|
|
|
|
|
|
|
|
|
|
xmkdirp (concat (new_root_path, "/dev", NULL), 0755);
|
|
|
|
|
devmount (new_root_path, "null");
|
|
|
|
|
devmount (new_root_path, "zero");
|
|
|
|
|
devmount (new_root_path, "urandom");
|
|
|
|
|
|
|
|
|
|
/* We're done with the "old" root, switch to the new one. */
|
|
|
|
|
if (chroot (new_root_path) < 0)
|
|
|
|
|
FAIL_EXIT1 ("Can't chroot to %s - ", new_root_path);
|
|
|
|
|
|
|
|
|
|
if (chdir (new_cwd_path) < 0)
|
|
|
|
|
FAIL_EXIT1 ("Can't cd to new %s - ", new_cwd_path);
|
|
|
|
|
|
|
|
|
|
/* To complete the containerization, we need to fork () at least
|
|
|
|
|
once. We can't exec, nor can we somehow link the new child to
|
|
|
|
|
our parent. So we run the child and propogate it's exit status
|
|
|
|
|
up. */
|
|
|
|
|
child = fork ();
|
|
|
|
|
if (child < 0)
|
|
|
|
|
FAIL_EXIT1 ("Unable to fork");
|
|
|
|
|
else if (child > 0)
|
|
|
|
|
{
|
|
|
|
|
/* Parent. */
|
|
|
|
|
int status;
|
|
|
|
|
waitpid (child, &status, 0);
|
|
|
|
|
|
|
|
|
|
/* There's a bit of magic here, since the buildroot is mounted
|
|
|
|
|
in our space, the paths are still valid, and since the mounts
|
|
|
|
|
aren't recursive, it sees *only* the built root, not anything
|
|
|
|
|
we would normally se if we rsync'd to "/" like mounted /dev
|
|
|
|
|
files. */
|
|
|
|
|
if (do_postclean)
|
|
|
|
|
rsync (pristine_root_path, new_root_path, 1);
|
|
|
|
|
|
|
|
|
|
if (WIFEXITED (status))
|
|
|
|
|
exit (WEXITSTATUS (status));
|
|
|
|
|
|
|
|
|
|
if (WIFSIGNALED (status))
|
|
|
|
|
{
|
|
|
|
|
printf ("%%SIGNALLED%%\n");
|
|
|
|
|
exit (77);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printf ("%%EXITERROR%%\n");
|
|
|
|
|
exit (78);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* The rest is the child process, which is now PID 1 and "in" the
|
|
|
|
|
new root. */
|
|
|
|
|
|
|
|
|
|
maybe_xmkdir ("/tmp", 0755);
|
|
|
|
|
|
|
|
|
|
/* Now that we're pid 1 (effectively "root") we can mount /proc */
|
|
|
|
|
maybe_xmkdir ("/proc", 0777);
|
|
|
|
|
if (mount ("proc", "/proc", "proc", 0, NULL) < 0)
|
|
|
|
|
FAIL_EXIT1 ("Unable to mount /proc: ");
|
|
|
|
|
|
|
|
|
|
/* We map our original UID to the same UID in the container so we
|
|
|
|
|
can own our own files normally. */
|
|
|
|
|
UMAP = open ("/proc/self/uid_map", O_WRONLY);
|
|
|
|
|
if (UMAP < 0)
|
|
|
|
|
FAIL_EXIT1 ("can't write to /proc/self/uid_map\n");
|
|
|
|
|
|
|
|
|
|
sprintf (tmp, "%lld %lld 1\n",
|
|
|
|
|
(long long) original_uid, (long long) original_uid);
|
|
|
|
|
write (UMAP, tmp, strlen (tmp));
|
|
|
|
|
xclose (UMAP);
|
|
|
|
|
|
|
|
|
|
/* We must disable setgroups () before we can map our groups, else we
|
|
|
|
|
get EPERM. */
|
|
|
|
|
GMAP = open ("/proc/self/setgroups", O_WRONLY);
|
|
|
|
|
if (GMAP >= 0)
|
|
|
|
|
{
|
|
|
|
|
/* We support kernels old enough to not have this. */
|
|
|
|
|
write (GMAP, "deny\n", 5);
|
|
|
|
|
xclose (GMAP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* We map our original GID to the same GID in the container so we
|
|
|
|
|
can own our own files normally. */
|
|
|
|
|
GMAP = open ("/proc/self/gid_map", O_WRONLY);
|
|
|
|
|
if (GMAP < 0)
|
|
|
|
|
FAIL_EXIT1 ("can't write to /proc/self/gid_map\n");
|
|
|
|
|
|
|
|
|
|
sprintf (tmp, "%lld %lld 1\n",
|
|
|
|
|
(long long) original_gid, (long long) original_gid);
|
|
|
|
|
write (GMAP, tmp, strlen (tmp));
|
|
|
|
|
xclose (GMAP);
|
|
|
|
|
|
|
|
|
|
/* Now run the child. */
|
|
|
|
|
execvp (new_child_proc[0], new_child_proc);
|
|
|
|
|
|
|
|
|
|
/* Or don't run the child? */
|
|
|
|
|
FAIL_EXIT1 ("Unable to exec %s\n", new_child_proc[0]);
|
|
|
|
|
|
|
|
|
|
/* Because gcc won't know error () never returns... */
|
|
|
|
|
exit (EXIT_UNSUPPORTED);
|
|
|
|
|
}
|