Add support for running multi-user in a chroot() environment.

How it works:

- after successful execution of /etc/rc, check the value of "init.root"
  sysctl node, if it's different than "/", chroot() into its value and run
  /etc/rc inside the chroot(),

- in single-user, return back to the original / file system.

Allows running with / file system on e.g., cgd(4), vnd(4) or ccd(4) volumes.

Idea first discussed with Matt Thomas, implemented by Jachym Holecek <freza
(at) liberouter.org> with some nitpicks by me.  Successfully used by me for
almost a year with / on a cgd(4) volume (for more information about the setup
check ftp://ftp.NetBSD.org/pub/NetBSD/misc/salo/init-chroot/ ).
This commit is contained in:
salo 2006-04-18 11:40:26 +00:00
parent c8d2679980
commit b0b7590bc7
4 changed files with 293 additions and 28 deletions

View File

@ -1,4 +1,4 @@
# $NetBSD: Makefile,v 1.35 2005/01/13 03:21:59 lukem Exp $
# $NetBSD: Makefile,v 1.36 2006/04/18 11:40:26 salo Exp $
# @(#)Makefile 8.1 (Berkeley) 7/19/93
PROG= init
@ -10,7 +10,7 @@ CPPFLAGS+= -DMFS_DEV_IF_NO_CONSOLE -DSUPPORT_UTMP -DSUPPORT_UTMPX
.ifdef SMALLPROG
CPPFLAGS+= -DLETS_GET_SMALL
.else
CPPFLAGS+= -DALTSHELL -DSECURE
CPPFLAGS+= -DALTSHELL -DSECURE -DCHROOT
DPADD+= ${LIBCRYPT}
LDADD+= -lcrypt
.endif

View File

@ -1,4 +1,4 @@
$NetBSD: NOTES,v 1.2 1995/03/18 14:56:29 cgd Exp $
$NetBSD: NOTES,v 1.3 2006/04/18 11:40:26 salo Exp $
POSIX and init:
--------------
@ -101,8 +101,13 @@ init is responsible for utmp and wtmp maintenance (ick)
necessary states and state transitions (gleaned from the man page):
1: single user shell (with password checking?); on exit, go to 2
2: rc script: on exit 0, go to 3; on exit N (error), go to 1
3: read ttys file: on completion, go to 4
2: run rc script, on exit 0 check if init.root sysctl != "/", if it
differs then fork + chroot into the value of init.root and run
/etc/rc inside the chroot: on exit 0, go to 3; on exit N (error),
go to 1 (applies also to /etc/rc when init.root == "/")
3: read ttys file: on completion, go to 4. If we did chroot in
state 2, we chroot after forking each getty to the same dir
(init.root is not re-read)
4: multi-user operation: on SIGTERM, go to 7; on SIGHUP, go to 5;
on SIGTSTP, go to 6
5: clean up mode (re-read ttys file, killing off controlling processes

View File

@ -1,4 +1,4 @@
.\" $NetBSD: init.8,v 1.37 2005/11/13 18:39:53 elad Exp $
.\" $NetBSD: init.8,v 1.38 2006/04/18 11:40:26 salo Exp $
.\"
.\" Copyright (c) 1980, 1991, 1993
.\" The Regents of the University of California. All rights reserved.
@ -32,7 +32,7 @@
.\"
.\" @(#)init.8 8.6 (Berkeley) 5/26/95
.\"
.Dd November 13, 2005
.Dd April 18, 2006
.Dt INIT 8
.Os
.Sh NAME
@ -78,11 +78,28 @@ exits with a non-zero (error) exit code, commence single user
operation by giving the super-user a shell on the console by going
to state 1 (single user).
Otherwise, proceed to state 3.
.Pp
If value of the
.Dq init.root
sysctl node is not equal to
.Pa /
at this point, the
.Pa /etc/rc
process will be run inside a
.Xr chroot 2
indicated by sysctl with the same error handling as above.
.It
Set up ttys as specified in
.Xr ttys 5 .
See below for more information.
On completion, continue to state 4.
If we did chroot in state 2, each
.Xr getty 8
process will be run in the same
.Xr chroot 2
path as in 2 (that is, the value of
.Dq init.root
sysctl is not re-read).
.It
Multi-user operation.
Depending upon the signal received, change state appropriately;
@ -130,6 +147,26 @@ The password check is skipped if the
is marked as
.Dq secure .
.Pp
It should be noted that while
.Nm
has the ability to start multi-user operation inside a
.Xr chroot 2
environment, the
.Nm
process itself will always run in the
.Dq original root directory .
This also implies that single-user mode is always started in the original
root, giving the possibility to create multi-user sessions in different
root directories over time. The
.Dq init.root
sysctl node is fabricated by
.Nm
at startup and re-created any time it's found to be missing. Type of the
node is string capable of holding full pathname, and is only accessible by
the superuser (unless explicitly destroyed and re-created with different
specification). The node becomes read-only after securelevel 1 has been
reached.
.Pp
The kernel runs with four different levels of security.
Any superuser process can raise the security level, but only
.Nm
@ -403,7 +440,8 @@ device driver because of a persistent device error condition.
.Xr mfs 8 ,
.Xr rc 8 ,
.Xr reboot 8 ,
.Xr shutdown 8
.Xr shutdown 8 ,
.Xr sysctl 8
.Sh HISTORY
A
.Nm

View File

@ -1,4 +1,4 @@
/* $NetBSD: init.c,v 1.72 2006/03/17 15:53:46 rumble Exp $ */
/* $NetBSD: init.c,v 1.73 2006/04/18 11:40:26 salo Exp $ */
/*-
* Copyright (c) 1991, 1993
@ -42,7 +42,7 @@ __COPYRIGHT("@(#) Copyright (c) 1991, 1993\n"
#if 0
static char sccsid[] = "@(#)init.c 8.2 (Berkeley) 4/28/95";
#else
__RCSID("$NetBSD: init.c,v 1.72 2006/03/17 15:53:46 rumble Exp $");
__RCSID("$NetBSD: init.c,v 1.73 2006/04/18 11:40:26 salo Exp $");
#endif
#endif /* not lint */
@ -135,12 +135,6 @@ state_func_t death(void);
enum { AUTOBOOT, FASTBOOT } runcom_mode = AUTOBOOT;
void transition(state_t);
#ifndef LETS_GET_SMALL
state_t requested_transition = runcom;
#else /* LETS_GET_SMALL */
state_t requested_transition = single_user;
#endif /* LETS_GET_SMALL */
void setctty(const char *);
typedef struct init_session {
@ -174,16 +168,31 @@ int getsecuritylevel(void);
int setupargv(session_t *, struct ttyent *);
int clang;
#ifndef LETS_GET_SMALL
void clear_session_logs(session_t *, int);
#endif
int start_session_db(void);
void add_session(session_t *);
void del_session(session_t *);
session_t *find_session(pid_t);
DB *session_db;
int do_setttyent(void);
#ifndef LETS_GET_SMALL
state_t requested_transition = runcom;
void clear_session_logs(session_t *, int);
state_func_t runetcrc(int);
#ifdef CHROOT
int did_multiuser_chroot = 0;
char rootdir[PATH_MAX];
int shouldchroot(void);
int createsysctlnode(void);
#endif /* CHROOT */
#else /* LETS_GET_SMALL */
state_t requested_transition = single_user;
#endif /* !LETS_GET_SMALL */
#ifdef MFS_DEV_IF_NO_CONSOLE
#define NINODE 1024
@ -309,6 +318,11 @@ main(int argc, char **argv)
(void)close(1);
(void)close(2);
#if !defined(LETS_GET_SMALL) && defined(CHROOT)
/* Create "init.root" sysctl node. */
createsysctlnode();
#endif /* !LETS_GET_SMALL && CHROOT*/
/*
* Start the state machine.
*/
@ -392,6 +406,29 @@ warning(const char *message, ...)
vsyslog(LOG_ALERT, message, ap);
va_end(ap);
closelog();
#if 0
/*
* XXX: syslog seems to just plain not work in console-only
* XXX: situation... that should be fixed. Let's leave this
* XXX: note + code here in case someone gets in trouble and
* XXX: wants to debug. -- Jachym Holecek <freza@liberouter.org>
*/
{
char errbuf[1024];
int fd, len;
/* We can't do anything on errors, anyway... */
fd = open(_PATH_CONSOLE, O_WRONLY);
if (fd == -1)
return ;
/* %m will get lost... */
len = vsnprintf(errbuf, sizeof(errbuf), message, ap);
(void)write(fd, (void *)errbuf, len);
(void)close(fd);
}
#endif
}
/*
@ -566,6 +603,11 @@ single_user(void)
char altshell[128];
#endif /* ALTSHELL */
#if !defined(LETS_GET_SMALL) && defined(CHROOT)
/* Clear previous idea, just in case. */
did_multiuser_chroot = 0;
#endif /* !LETS_GET_SMALL && CHROOT */
/*
* If the kernel is in secure mode, downgrade it to insecure mode.
*/
@ -722,11 +764,10 @@ single_user(void)
}
#ifndef LETS_GET_SMALL
/*
* Run the system startup script.
*/
/* ARGSUSED */
state_func_t
runcom(void)
runetcrc(int trychroot)
{
pid_t pid, wpid;
int status;
@ -745,11 +786,20 @@ runcom(void)
argv[0] = "sh";
argv[1] = _PATH_RUNCOM;
argv[2] = runcom_mode == AUTOBOOT ? "autoboot" : 0;
argv[2] = (runcom_mode == AUTOBOOT ? "autoboot" : 0);
argv[3] = 0;
(void)sigprocmask(SIG_SETMASK, &sa.sa_mask, NULL);
#ifdef CHROOT
if (trychroot)
if (chroot(rootdir) != 0) {
warning("failed to chroot to %s, error: %m",
rootdir);
_exit(1); /* force single user mode */
}
#endif /* CHROOT */
(void)execv(INIT_BSHELL, __UNCONST(argv));
stall("can't exec %s for %s: %m", INIT_BSHELL, _PATH_RUNCOM);
_exit(1); /* force single user mode */
@ -805,6 +855,43 @@ runcom(void)
if (WEXITSTATUS(status))
return (state_func_t)single_user;
return (state_func_t) read_ttys;
}
/*
* Run the system startup script.
*/
state_func_t
runcom(void)
{
state_func_t next_step;
/* Run /etc/rc and choose next state depending on the result. */
next_step = runetcrc(0);
if (next_step != (state_func_t) read_ttys)
return (state_func_t) next_step;
#ifdef CHROOT
/*
* If init.root sysctl does not point to "/", we'll chroot and run
* The Real(tm) /etc/rc now. Global variable rootdir will tell us
* where to go.
*/
if (shouldchroot()) {
next_step = runetcrc(1);
if (next_step != (state_func_t) read_ttys)
return (state_func_t) next_step;
did_multiuser_chroot = 1;
} else {
did_multiuser_chroot = 0;
}
#endif /* CHROOT */
/*
* Regardless of whether in chroot or not, we booted successfuly.
* It's time to spawn gettys (ie. next_step's value at this point).
*/
runcom_mode = AUTOBOOT; /* the default */
/* NB: should send a message to the session logger to avoid blocking. */
#ifdef SUPPORT_UTMPX
@ -1034,8 +1121,19 @@ read_ttys(void)
free_session(sp);
}
sessions = NULL;
if (start_session_db())
if (start_session_db()) {
warning("read_ttys: start_session_db failed, death\n");
#ifdef CHROOT
/* If /etc/rc ran in chroot, we want to kill any survivors. */
if (did_multiuser_chroot)
return (state_func_t)death;
else
#endif /* CHROOT */
return (state_func_t)single_user;
}
do_setttyent();
/*
* Allocate a session entry for each active port.
@ -1044,7 +1142,6 @@ read_ttys(void)
while ((typ = getttyent()) != NULL)
if ((snext = new_session(sp, ++session_index, typ)) != NULL)
sp = snext;
endttyent();
return (state_func_t)multi_user;
@ -1102,6 +1199,16 @@ start_getty(session_t *sp)
if (pid)
return pid;
#ifdef CHROOT
/* If /etc/rc did proceed inside chroot, we have to try as well. */
if (did_multiuser_chroot)
if (chroot(rootdir) != 0) {
stall("can't chroot getty '%s' inside %s: %m",
sp->se_getty_argv[0], rootdir);
_exit(1);
}
#endif /* CHROOT */
if (current_time > sp->se_started &&
current_time - sp->se_started < GETTY_SPACING) {
warning("getty repeating too quickly on port %s, sleeping",
@ -1249,6 +1356,8 @@ clean_ttys(void)
for (sp = sessions; sp; sp = sp->se_next)
sp->se_flags &= ~SE_PRESENT;
do_setttyent();
devlen = sizeof(_PATH_DEV) - 1;
while ((typ = getttyent()) != NULL) {
++session_index;
@ -1501,6 +1610,7 @@ mfs_dev(void)
mfile[0].len ? "./MAKEDEV" : "/etc/MAKEDEV",
"init", NULL);
_exit(1);
/* NOTREACHED */
case -1:
break;
@ -1518,3 +1628,115 @@ mfs_dev(void)
return (-1);
}
#endif
int
do_setttyent(void)
{
endttyent();
#ifdef CHROOT
if (did_multiuser_chroot) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", rootdir, _PATH_TTYS);
return setttyentpath(path);
} else
#endif /* CHROOT */
return setttyent();
}
#if !defined(LETS_GET_SMALL) && defined(CHROOT)
int
createsysctlnode()
{
struct sysctlnode node;
int mib[2];
size_t len;
/*
* Create top-level dynamic sysctl node. Its child nodes will only
* be readable by the superuser, since regular mortals should not
* care ("Sssh, it's a secret!"). Additionally, both nodes become
* read-only at securelevel 1.
*/
len = sizeof(struct sysctlnode);
mib[0] = CTL_CREATE;
memset(&node, 0, len);
node.sysctl_flags = SYSCTL_VERSION | CTLFLAG_READWRITE |
CTLFLAG_PRIVATE | CTLFLAG_READONLY1 | CTLTYPE_NODE;
node.sysctl_num = CTL_CREATE;
snprintf(node.sysctl_name, SYSCTL_NAMELEN, "init");
if (sysctl(&mib[0], 1, &node, &len, &node, len) == -1) {
warning("could not create init node, error = %d", errno);
return (-1);
}
/*
* Create second level dynamic node capable of holding pathname.
* Provide "/" as the default value.
*/
len = sizeof(struct sysctlnode);
mib[0] = node.sysctl_num;
mib[1] = CTL_CREATE;
memset(&node, 0, len);
node.sysctl_flags = SYSCTL_VERSION | CTLFLAG_READWRITE |
CTLFLAG_READONLY1 | CTLTYPE_STRING | CTLFLAG_OWNDATA;
node.sysctl_size = _POSIX_PATH_MAX;
node.sysctl_data = __UNCONST("/");
node.sysctl_num = CTL_CREATE;
snprintf(node.sysctl_name, SYSCTL_NAMELEN, "root");
if (sysctl(&mib[0], 2, NULL, NULL, &node, len) == -1) {
warning("could not create init.root node, error = %d", errno);
return (-1);
}
return (0);
}
int
shouldchroot()
{
struct sysctlnode node;
size_t len, cnt;
int mib;
len = sizeof(struct sysctlnode);
if (sysctlbyname("init.root", rootdir, &len, NULL, 0) == -1) {
warning("could not read init.root, error = %d", errno);
/* Child killed our node. Recreate it. */
if (errno == ENOENT) {
/* Destroy whatever is left, recreate from scratch. */
if (sysctlnametomib("init", &mib, &cnt) != -1) {
memset(&node, 0, sizeof(node));
node.sysctl_flags = SYSCTL_VERSION;
node.sysctl_num = mib;
mib = CTL_DESTROY;
(void)sysctl(&mib, 1, NULL, NULL, &node,
sizeof(node));
}
createsysctlnode();
}
/* We certainly won't chroot. */
return (0);
}
if (rootdir[len] != '\0' || strlen(rootdir) != len - 1) {
warning("init.root is not a string");
return (0);
}
if (strcmp(rootdir, "/") == 0)
return (0);
return (1);
}
#endif /* !LETS_GET_SMALL && CHROOT */