libvirt/tools/virt-login-shell-helper.c

422 lines
11 KiB
C
Raw Normal View History

/*
* virt-login-shell-helper.c: a shell to connect to a container
*
* Copyright (C) 2013-2014 Red Hat, Inc.
*
* This 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.
*
* This 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 this library. If not, see
* <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <getopt.h>
#include <signal.h>
#include <unistd.h>
#include "internal.h"
#include "virerror.h"
#include "virconf.h"
#include "virutil.h"
#include "virfile.h"
#include "virprocess.h"
#include "configmake.h"
#include "virstring.h"
#include "viralloc.h"
#include "vircommand.h"
#include "virgettext.h"
#define VIR_FROM_THIS VIR_FROM_NONE
static const char *conf_file = SYSCONFDIR "/libvirt/virt-login-shell.conf";
static int virLoginShellAllowedUser(virConf *conf,
const char *name,
gid_t *groups,
size_t ngroups)
{
int ret = -1;
size_t i;
char *gname = NULL;
g_auto(GStrv) users = NULL;
char **entries;
if (virConfGetValueStringList(conf, "allowed_users", false, &users) < 0)
goto cleanup;
for (entries = users; entries && *entries; entries++) {
char *entry = *entries;
/*
If string begins with a % this indicates a linux group.
Check to see if the user is in the Linux Group.
*/
if (entry[0] == '%') {
entry++;
if (!*entry)
continue;
for (i = 0; i < ngroups; i++) {
if (!(gname = virGetGroupName(groups[i])))
continue;
if (g_pattern_match_simple(entry, gname)) {
ret = 0;
goto cleanup;
}
VIR_FREE(gname);
}
} else {
if (g_pattern_match_simple(entry, name)) {
ret = 0;
goto cleanup;
}
}
}
virReportSystemError(EPERM,
_("%s not matched against 'allowed_users' in %s"),
name, conf_file);
cleanup:
VIR_FREE(gname);
return ret;
}
static int virLoginShellGetShellArgv(virConf *conf,
char ***shargv,
size_t *shargvlen)
{
int rv;
if ((rv = virConfGetValueStringList(conf, "shell", true, shargv)) < 0)
return -1;
if (rv == 0) {
*shargv = g_new0(char *, 2);
(*shargv)[0] = g_strdup("/bin/sh");
*shargvlen = 1;
} else {
*shargvlen = g_strv_length(*shargv);
}
return 0;
}
static char *progname;
/*
* Print usage
*/
static void
usage(void)
{
fprintf(stdout,
_("\n"
"Usage:\n"
" %s [option]\n\n"
"Options:\n"
" -h | --help Display program help\n"
" -V | --version Display program version\n"
" -c CMD Run CMD via shell\n"
"\n"
"libvirt login shell\n"),
progname);
return;
}
/* Display version information. */
static void
show_version(void)
{
printf("%s (%s) %s\n", progname, PACKAGE_NAME, PACKAGE_VERSION);
}
static void
hideErrorFunc(void *opaque G_GNUC_UNUSED,
virErrorPtr err G_GNUC_UNUSED)
{
}
int
main(int argc, char **argv)
{
g_autoptr(virConf) conf = NULL;
const char *login_shell_path = conf_file;
pid_t cpid = -1;
int ret = EXIT_CANCELED;
int status;
tools: split virt-login-shell into two binaries The virt-login-shell binary is a setuid program that takes no arguments. When invoked it looks at the invoking uid, resolves it to a username, and finds an LXC guest with the same name. It then starts the guest and runs the shell in side the namespaces of the container. Given this set of tasks the virt-login-shell binary needs to connect to libvirtd, make various other libvirt API calls. This is a problem for setuid binaries as various libraries that libvirt.so links to are not safe. For example, they have constructor functions which execute an unknown amount of code that can be influenced by env variables. For this reason virt-login-shell doesn't use libvirt.so, but instead links to a custom, cut down, set of source files sufficient to be a local client only. This introduces a problem for integrating glib2 into libvirt though, as once integrated, there would be no way to build virt-login-shell without an external dependancy on glib2 and this is definitely not setuid safe. To resolve this problem, we split the virt-login-shell binary into two parts. The first part is setuid and does almost nothing. It simply records the original uid+gid, and then invokes the virt-login-shell-helper binary. Crucially when it does this it completes scrubs all environment variables. It is thus safe for virt-login-shell-helper to link to the normal libvirt.so. Any things that constructor functions do cannot be influenced by user control env vars or cli args. Reviewed-by: Michal Privoznik <mprivozn@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
2019-08-01 09:58:31 +00:00
unsigned long long uidval;
unsigned long long gidval;
uid_t uid;
gid_t gid;
char *name = NULL;
g_auto(GStrv) shargv = NULL;
size_t shargvlen = 0;
char *shcmd = NULL;
virSecurityModelPtr secmodel = NULL;
virSecurityLabelPtr seclabel = NULL;
virDomainPtr dom = NULL;
virConnectPtr conn = NULL;
char *homedir = NULL;
int arg;
int longindex = -1;
int ngroups;
gid_t *groups = NULL;
ssize_t nfdlist = 0;
int *fdlist = NULL;
int openmax;
size_t i;
const char *cmdstr = NULL;
char *tmp;
char *term = NULL;
virErrorPtr saved_err = NULL;
bool autoshell = false;
struct option opt[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", optional_argument, NULL, 'V' },
{ NULL, 0, NULL, 0 },
};
if (virInitialize() < 0) {
fprintf(stderr, _("Failed to initialize libvirt error handling"));
return EXIT_CANCELED;
}
virSetErrorFunc(NULL, hideErrorFunc);
virSetErrorLogPriorityFunc(NULL);
progname = argv[0];
if (virGettextInitialize() < 0)
return ret;
tools: split virt-login-shell into two binaries The virt-login-shell binary is a setuid program that takes no arguments. When invoked it looks at the invoking uid, resolves it to a username, and finds an LXC guest with the same name. It then starts the guest and runs the shell in side the namespaces of the container. Given this set of tasks the virt-login-shell binary needs to connect to libvirtd, make various other libvirt API calls. This is a problem for setuid binaries as various libraries that libvirt.so links to are not safe. For example, they have constructor functions which execute an unknown amount of code that can be influenced by env variables. For this reason virt-login-shell doesn't use libvirt.so, but instead links to a custom, cut down, set of source files sufficient to be a local client only. This introduces a problem for integrating glib2 into libvirt though, as once integrated, there would be no way to build virt-login-shell without an external dependancy on glib2 and this is definitely not setuid safe. To resolve this problem, we split the virt-login-shell binary into two parts. The first part is setuid and does almost nothing. It simply records the original uid+gid, and then invokes the virt-login-shell-helper binary. Crucially when it does this it completes scrubs all environment variables. It is thus safe for virt-login-shell-helper to link to the normal libvirt.so. Any things that constructor functions do cannot be influenced by user control env vars or cli args. Reviewed-by: Michal Privoznik <mprivozn@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
2019-08-01 09:58:31 +00:00
if (geteuid() != 0) {
fprintf(stderr, _("%s: must be run as root\n"), argv[0]);
return ret;
}
if (getuid() != 0) {
fprintf(stderr, _("%s: must not be run setuid root\n"), argv[0]);
return ret;
}
while ((arg = getopt_long(argc, argv, "hVc:", opt, &longindex)) != -1) {
switch (arg) {
case 'h':
usage();
exit(EXIT_SUCCESS);
case 'V':
show_version();
exit(EXIT_SUCCESS);
case 'c':
cmdstr = optarg;
break;
case '?':
default:
usage();
exit(EXIT_CANCELED);
}
}
tools: split virt-login-shell into two binaries The virt-login-shell binary is a setuid program that takes no arguments. When invoked it looks at the invoking uid, resolves it to a username, and finds an LXC guest with the same name. It then starts the guest and runs the shell in side the namespaces of the container. Given this set of tasks the virt-login-shell binary needs to connect to libvirtd, make various other libvirt API calls. This is a problem for setuid binaries as various libraries that libvirt.so links to are not safe. For example, they have constructor functions which execute an unknown amount of code that can be influenced by env variables. For this reason virt-login-shell doesn't use libvirt.so, but instead links to a custom, cut down, set of source files sufficient to be a local client only. This introduces a problem for integrating glib2 into libvirt though, as once integrated, there would be no way to build virt-login-shell without an external dependancy on glib2 and this is definitely not setuid safe. To resolve this problem, we split the virt-login-shell binary into two parts. The first part is setuid and does almost nothing. It simply records the original uid+gid, and then invokes the virt-login-shell-helper binary. Crucially when it does this it completes scrubs all environment variables. It is thus safe for virt-login-shell-helper to link to the normal libvirt.so. Any things that constructor functions do cannot be influenced by user control env vars or cli args. Reviewed-by: Michal Privoznik <mprivozn@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
2019-08-01 09:58:31 +00:00
if (optind != (argc - 2)) {
virReportSystemError(EINVAL, _("%s expects UID and GID parameters"), progname);
goto cleanup;
}
tools: split virt-login-shell into two binaries The virt-login-shell binary is a setuid program that takes no arguments. When invoked it looks at the invoking uid, resolves it to a username, and finds an LXC guest with the same name. It then starts the guest and runs the shell in side the namespaces of the container. Given this set of tasks the virt-login-shell binary needs to connect to libvirtd, make various other libvirt API calls. This is a problem for setuid binaries as various libraries that libvirt.so links to are not safe. For example, they have constructor functions which execute an unknown amount of code that can be influenced by env variables. For this reason virt-login-shell doesn't use libvirt.so, but instead links to a custom, cut down, set of source files sufficient to be a local client only. This introduces a problem for integrating glib2 into libvirt though, as once integrated, there would be no way to build virt-login-shell without an external dependancy on glib2 and this is definitely not setuid safe. To resolve this problem, we split the virt-login-shell binary into two parts. The first part is setuid and does almost nothing. It simply records the original uid+gid, and then invokes the virt-login-shell-helper binary. Crucially when it does this it completes scrubs all environment variables. It is thus safe for virt-login-shell-helper to link to the normal libvirt.so. Any things that constructor functions do cannot be influenced by user control env vars or cli args. Reviewed-by: Michal Privoznik <mprivozn@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
2019-08-01 09:58:31 +00:00
if (virStrToLong_ull(argv[optind], NULL, 10, &uidval) < 0 ||
((uid_t)uidval) != uidval) {
virReportSystemError(EINVAL, _("%s cannot parse UID '%s'"),
progname, argv[optind]);
goto cleanup;
}
tools: split virt-login-shell into two binaries The virt-login-shell binary is a setuid program that takes no arguments. When invoked it looks at the invoking uid, resolves it to a username, and finds an LXC guest with the same name. It then starts the guest and runs the shell in side the namespaces of the container. Given this set of tasks the virt-login-shell binary needs to connect to libvirtd, make various other libvirt API calls. This is a problem for setuid binaries as various libraries that libvirt.so links to are not safe. For example, they have constructor functions which execute an unknown amount of code that can be influenced by env variables. For this reason virt-login-shell doesn't use libvirt.so, but instead links to a custom, cut down, set of source files sufficient to be a local client only. This introduces a problem for integrating glib2 into libvirt though, as once integrated, there would be no way to build virt-login-shell without an external dependancy on glib2 and this is definitely not setuid safe. To resolve this problem, we split the virt-login-shell binary into two parts. The first part is setuid and does almost nothing. It simply records the original uid+gid, and then invokes the virt-login-shell-helper binary. Crucially when it does this it completes scrubs all environment variables. It is thus safe for virt-login-shell-helper to link to the normal libvirt.so. Any things that constructor functions do cannot be influenced by user control env vars or cli args. Reviewed-by: Michal Privoznik <mprivozn@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
2019-08-01 09:58:31 +00:00
optind++;
if (virStrToLong_ull(argv[optind], NULL, 10, &gidval) < 0 ||
((gid_t)gidval) != gidval) {
virReportSystemError(EINVAL, _("%s cannot parse GID '%s'"),
progname, argv[optind]);
goto cleanup;
}
uid = (uid_t)uidval;
gid = (gid_t)gidval;
name = virGetUserName(uid);
if (!name)
goto cleanup;
homedir = virGetUserDirectoryByUID(uid);
if (!homedir)
goto cleanup;
if (!(conf = virConfReadFile(login_shell_path, 0)))
goto cleanup;
if ((ngroups = virGetGroupList(uid, gid, &groups)) < 0)
goto cleanup;
if (virLoginShellAllowedUser(conf, name, groups, ngroups) < 0)
goto cleanup;
if (virLoginShellGetShellArgv(conf, &shargv, &shargvlen) < 0)
goto cleanup;
if (virConfGetValueBool(conf, "auto_shell", &autoshell) < 0)
goto cleanup;
conn = virConnectOpen("lxc:///system");
if (!conn)
goto cleanup;
dom = virDomainLookupByName(conn, name);
if (!dom)
goto cleanup;
if (!virDomainIsActive(dom) && virDomainCreate(dom) < 0) {
virErrorPtr last_error;
last_error = virGetLastError();
if (last_error->code != VIR_ERR_OPERATION_INVALID) {
virReportSystemError(last_error->code,
_("Can't create %s container: %s"),
name, last_error->message);
goto cleanup;
}
}
openmax = sysconf(_SC_OPEN_MAX);
if (openmax < 0) {
virReportSystemError(errno, "%s",
_("sysconf(_SC_OPEN_MAX) failed"));
goto cleanup;
}
if ((nfdlist = virDomainLxcOpenNamespace(dom, &fdlist, 0)) < 0)
goto cleanup;
secmodel = g_new0(virSecurityModel, 1);
seclabel = g_new0(virSecurityLabel, 1);
if (virNodeGetSecurityModel(conn, secmodel) < 0)
goto cleanup;
if (virDomainGetSecurityLabel(dom, seclabel) < 0)
goto cleanup;
if (virSetUIDGID(0, 0, NULL, 0) < 0)
goto cleanup;
if (virDomainLxcEnterSecurityLabel(secmodel, seclabel, NULL, 0) < 0)
goto cleanup;
if (virDomainLxcEnterCGroup(dom, 0) < 0)
goto cleanup;
if (nfdlist > 0 &&
virDomainLxcEnterNamespace(dom, nfdlist, fdlist, NULL, NULL, 0) < 0)
goto cleanup;
if (virSetUIDGID(uid, gid, groups, ngroups) < 0)
goto cleanup;
if (chdir(homedir) < 0) {
virReportSystemError(errno, _("Unable to chdir(%s)"), homedir);
goto cleanup;
}
if (autoshell) {
tmp = virGetUserShell(uid);
if (tmp) {
g_strfreev(shargv);
shargvlen = 1;
shargv = g_new0(char *, shargvlen + 1);
shargv[0] = tmp;
shargv[1] = NULL;
}
}
if (cmdstr) {
VIR_REALLOC_N(shargv, shargvlen + 3);
shargv[shargvlen++] = g_strdup("-c");
shargv[shargvlen++] = g_strdup(cmdstr);
shargv[shargvlen] = NULL;
}
/* We need to modify the first elementin shargv
* so that it has the relative filename and has
* a leading '-' to indicate it is a login shell
*/
shcmd = shargv[0];
if (!g_path_is_absolute(shcmd)) {
virReportSystemError(errno,
_("Shell '%s' should have absolute path"),
shcmd);
goto cleanup;
}
tmp = strrchr(shcmd, '/');
shargv[0] = g_strdup(tmp);
shargv[0][0] = '-';
/* We're duping the string because the clearenv()
* call will shortly release the pointer we get
* back from getenv() right here */
term = g_strdup(getenv("TERM"));
/* A fork is required to create new process in correct pid namespace. */
virFork: simplify semantics The old semantics of virFork() violates the priciple of good usability: it requires the caller to check the pid argument after use, *even when virFork returned -1*, in order to properly abort a child process that failed setup done immediately after fork() - that is, the caller must call _exit() in the child. While uses in virfile.c did this correctly, uses in 'virsh lxc-enter-namespace' and 'virt-login-shell' would happily return from the calling function in both the child and the parent, leading to very confusing results. [Thankfully, I found the problem by inspection, and can't actually trigger the double return on error without an LD_PRELOAD library.] It is much better if the semantics of virFork are impossible to abuse. Looking at virFork(), the parent could only ever return -1 with a non-negative pid if it misused pthread_sigmask, but this never happens. Up until this patch series, the child could return -1 with non-negative pid if it fails to set up signals correctly, but we recently fixed that to make the child call _exit() at that point instead of forcing the caller to do it. Thus, the return value and contents of the pid argument are now redundant (a -1 return now happens only for failure to fork, a child 0 return only happens for a successful 0 pid, and a parent 0 return only happens for a successful non-zero pid), so we might as well return the pid directly rather than an integer of whether it succeeded or failed; this is also good from the interface design perspective as users are already familiar with fork() semantics. One last change in this patch: before returning the pid directly, I found cases where using virProcessWait unconditionally on a cleanup path of a virFork's -1 pid return would be nicer if there were a way to avoid it overwriting an earlier message. While such paths are a bit harder to come by with my change to a direct pid return, I decided to keep the virProcessWait change in this patch. * src/util/vircommand.h (virFork): Change signature. * src/util/vircommand.c (virFork): Guarantee that child will only return on success, to simplify callers. Return pid rather than status, now that the situations are always the same. (virExec): Adjust caller, also avoid open-coding process death. * src/util/virprocess.c (virProcessWait): Tweak semantics when pid is -1. (virProcessRunInMountNamespace): Adjust caller. * src/util/virfile.c (virFileAccessibleAs, virFileOpenForked) (virDirCreate): Likewise. * tools/virt-login-shell.c (main): Likewise. * tools/virsh-domain.c (cmdLxcEnterNamespace): Likewise. * tests/commandtest.c (test23): Likewise. Signed-off-by: Eric Blake <eblake@redhat.com>
2013-12-22 00:54:33 +00:00
if ((cpid = virFork()) < 0)
goto cleanup;
if (cpid == 0) {
int tmpfd;
for (i = 3; i < openmax; i++) {
tmpfd = i;
VIR_MASS_CLOSE(tmpfd);
}
clearenv();
g_setenv("PATH", "/bin:/usr/bin", TRUE);
g_setenv("SHELL", shcmd, TRUE);
g_setenv("USER", name, TRUE);
g_setenv("LOGNAME", name, TRUE);
g_setenv("HOME", homedir, TRUE);
if (term)
g_setenv("TERM", term, TRUE);
if (execv(shcmd, (char *const*) shargv) < 0) {
virReportSystemError(errno, _("Unable to exec shell %s"),
shcmd);
virDispatchError(NULL);
return errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
}
}
/* At this point, the parent is now waiting for the child to exit,
* but as that may take a long time, we release resources now. */
cleanup:
saved_err = virSaveLastError();
if (nfdlist > 0)
for (i = 0; i < nfdlist; i++)
VIR_FORCE_CLOSE(fdlist[i]);
VIR_FREE(fdlist);
if (dom)
virDomainFree(dom);
if (conn)
virConnectClose(conn);
VIR_FREE(shcmd);
VIR_FREE(term);
VIR_FREE(name);
VIR_FREE(homedir);
VIR_FREE(seclabel);
VIR_FREE(secmodel);
VIR_FREE(groups);
if (virProcessWait(cpid, &status, true) == 0)
virProcessExitWithStatus(status);
if (saved_err) {
virSetError(saved_err);
fprintf(stderr, "%s: %s\n", argv[0], virGetLastErrorMessage());
}
return ret;
}