/* Copyright © 2005-2009 Roger Leigh * * schroot is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * schroot 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * . * *********************************************************************/ #include #include "sbuild-chroot-config.h" #include "sbuild-chroot-facet-personality.h" #include "sbuild-chroot-facet-session.h" #include "sbuild-chroot-facet-session-clonable.h" #include "sbuild-auth-null.h" #include "sbuild-ctty.h" #include "sbuild-run-parts.h" #include "sbuild-session.h" #include "sbuild-util.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using std::cout; using std::endl; using boost::format; using namespace sbuild; namespace { typedef std::pair emap; /** * This is a list of the supported error codes. It's used to * construct the real error codes map. */ emap init_errors[] = { // TRANSLATORS: %1% = directory emap(session::CHDIR, N_("Failed to change to directory '%1%'")), // TRANSLATORS: %4% = directory emap(session::CHDIR_FB, N_("Falling back to directory '%4%'")), emap(session::CHILD_CORE, N_("Child dumped core")), emap(session::CHILD_FAIL, N_("Child exited abnormally (reason unknown; not a signal or core dump)")), emap(session::CHILD_FORK, N_("Failed to fork child")), // TRANSLATORS: %4% = signal name emap(session::CHILD_SIGNAL, N_("Child terminated by signal '%4%'")), emap(session::CHILD_WAIT, N_("Wait for child failed")), // TRANSLATORS: %1% = directory emap(session::CHROOT, N_("Failed to change root to directory '%1%'")), // TRANSLATORS: %1% = chroot name emap(session::CHROOT_ALIAS, N_("No chroot found matching name or alias '%1%'")), emap(session::CHROOT_LOCK, N_("Failed to lock chroot")), emap(session::CHROOT_SETUP, N_("Chroot setup failed")), // TRANSLATORS: %1% = chroot name emap(session::CHROOT_UNKNOWN, N_("Failed to find chroot '%1%'")), emap(session::CHROOT_UNLOCK, N_("Failed to unlock chroot")), // TRANSLATORS: %1% = command emap(session::COMMAND_ABS, N_("Command \"%1%\" must have an absolute path")), // TRANSLATORS: %1% = command emap(session::EXEC, N_("Failed to execute \"%1%\"")), // TRANSLATORS: A supplementary group is the list of additional // system groups a user belongs to, in addition to their default // group. emap(session::GROUP_GET_SUP, N_("Failed to get supplementary groups")), // TRANSLATORS: A supplementary group is the list of additional // system groups a user belongs to, in addition to their default // group. emap(session::GROUP_GET_SUPC, N_("Failed to get supplementary group count")), // TRANSLATORS: %1% = integer group ID emap(session::GROUP_SET, N_("Failed to set group '%1%'")), emap(session::GROUP_SET_SUP, N_("Failed to set supplementary groups")), // TRANSLATORS: %1% = group name emap(session::GROUP_UNKNOWN, N_("Group '%1%' not found")), emap(session::PAM, N_("PAM error")), emap(session::ROOT_DROP, N_("Failed to drop root permissions")), // TRANSLATORS: %1% = chroot name // TRANSLATORS: %4% = session identifier emap(session::SET_SESSION_ID, N_("%1%: Chroot does not support setting a session ID; ignoring session ID '%4%'")), // TRANSLATORS: %1% = command emap(session::SHELL, N_("Shell '%1%' not available")), // TRANSLATORS: %4% = command emap(session::SHELL_FB, N_("Falling back to shell '%4%'")), // TRANSLATORS: %4% = signal name emap(session::SIGNAL_CATCH, N_("Caught signal '%4%'")), // TRANSLATORS: %4% = signal name emap(session::SIGNAL_SET, N_("Failed to set signal handler '%4%'")), // TRANSLATORS: %1% = integer user ID emap(session::USER_SET, N_("Failed to set user '%1%'")), // TRANSLATORS: %1% = user name // TRANSLATORS: %2% = user name // TRANSLATORS: Please translate "->" as a right arrow, e.g. U+2192 emap(session::USER_SWITCH, N_("(%1%->%2%): User switching is not permitted")), }; /** * Check group membership. * * @param group the group to check for. * @returns true if the user is a member of group, otherwise false. */ bool is_group_member (std::string const& groupname) { errno = 0; sbuild::group grp(groupname); if (!grp) { if (errno == 0) { session::error e(groupname, session::GROUP_UNKNOWN); log_exception_warning(e); } else { session::error e(groupname, session::GROUP_UNKNOWN, strerror(errno)); log_exception_warning(e); } return false; } bool group_member = false; if (grp.gr_gid == getgid()) { group_member = true; } else { int supp_group_count = getgroups(0, 0); if (supp_group_count < 0) throw session::error(session::GROUP_GET_SUPC, strerror(errno)); if (supp_group_count > 0) { gid_t *supp_groups = new gid_t[supp_group_count]; assert (supp_groups); if (getgroups(supp_group_count, supp_groups) < 1) { // Free supp_groups before throwing to avoid leak. delete[] supp_groups; throw session::error(session::GROUP_GET_SUP, strerror(errno)); } for (int i = 0; i < supp_group_count; ++i) { if (grp.gr_gid == supp_groups[i]) group_member = true; } delete[] supp_groups; } } return group_member; } volatile bool sighup_called = false; volatile bool sigterm_called = false; /** * Handle the SIGHUP signal. * * @param ignore the signal number. */ void sighup_handler (int ignore) { /* This exists so that system calls get interrupted. */ sighup_called = true; } /** * Handle the SIGTERM signal. * * @param ignore the signal number. */ void sigterm_handler (int ignore) { /* This exists so that system calls get interrupted. */ sigterm_called = true; } #ifdef SBUILD_DEBUG volatile bool child_wait = true; #endif } template<> error::map_type error::error_strings (init_errors, init_errors + (sizeof(init_errors) / sizeof(init_errors[0]))); session::session (std::string const& service, config_ptr& config, operation operation, sbuild::string_list const& chroots): authstat(auth_null::create(service)), config(config), chroots(chroots), chroot_status(true), lock_status(true), child_status(EXIT_FAILURE), session_operation(operation), session_id(), force(false), saved_sighup_signal(), saved_sigterm_signal(), saved_termios(), termios_ok(false), cwd(sbuild::getcwd()) { } session::~session () { } auth::ptr const& session::get_auth () const { return this->authstat; } void session::set_auth (auth::ptr& auth) { this->authstat = auth; } session::config_ptr const& session::get_config () const { return this->config; } void session::set_config (config_ptr& config) { this->config = config; } string_list const& session::get_chroots () const { return this->chroots; } void session::set_chroots (string_list const& chroots) { this->chroots = chroots; } session::operation session::get_operation () const { return this->session_operation; } void session::set_operation (operation operation) { this->session_operation = operation; } std::string const& session::get_session_id () const { return this->session_id; } void session::set_session_id (std::string const& session_id) { this->session_id = session_id; } bool session::get_force () const { return this->force; } void session::set_force (bool force) { this->force = force; } void session::save_termios () { string_list const& command(this->authstat->get_command()); this->termios_ok = false; // Save if running a login shell and have a controlling terminal. if (CTTY_FILENO >= 0 && (command.empty() || command[0].empty())) { if (tcgetattr(CTTY_FILENO, &this->saved_termios) < 0) { sbuild::log_warning() << _("Error saving terminal settings") << endl; } else this->termios_ok = true; } } void session::restore_termios () { string_list const& command(this->authstat->get_command()); // Restore if running a login shell and have a controlling terminal, // and have previously saved the terminal state. if (CTTY_FILENO >= 0 && (command.empty() || command[0].empty()) && termios_ok) { if (tcsetattr(CTTY_FILENO, TCSANOW, &this->saved_termios) < 0) sbuild::log_warning() << _("Error restoring terminal settings") << endl; } } int session::get_child_status () const { return this->child_status; } void session::get_chroot_membership (chroot::ptr const& chroot, bool& in_users, bool& in_root_users, bool& in_groups, bool& in_root_groups) const { string_list const& users = chroot->get_users(); string_list const& root_users = chroot->get_root_users(); string_list const& groups = chroot->get_groups(); string_list const& root_groups = chroot->get_root_groups(); in_users = false; in_root_users = false; in_groups = false; in_root_groups = false; sbuild::string_list::const_iterator upos = find(users.begin(), users.end(), this->authstat->get_ruser()); if (upos != users.end()) in_users = true; sbuild::string_list::const_iterator rupos = find(root_users.begin(), root_users.end(), this->authstat->get_ruser()); if (rupos != root_users.end()) in_root_users = true; if (!groups.empty()) { for (string_list::const_iterator gp = groups.begin(); gp != groups.end(); ++gp) if (is_group_member(*gp)) in_groups = true; } if (!root_groups.empty()) { for (string_list::const_iterator gp = root_groups.begin(); gp != root_groups.end(); ++gp) if (is_group_member(*gp)) in_root_groups = true; } log_debug(DEBUG_INFO) << "In users: " << in_users << endl << "In groups: " << in_groups << endl << "In root-users: " << in_root_users << endl << "In root-groups: " << in_root_groups << endl; } auth::status session::get_chroot_auth_status (auth::status status, chroot::ptr const& chroot) const { bool in_users = false; bool in_root_users = false; bool in_groups = false; bool in_root_groups = false; get_chroot_membership(chroot, in_users, in_root_users, in_groups, in_root_groups); /* * No auth required if in root users or root groups and * changing to root, or if the uid is not changing. If not * in user or group, authentication fails immediately. */ if ((in_users == true || in_groups == true || in_root_users == true || in_root_groups == true) && this->authstat->get_ruid() == this->authstat->get_uid()) { status = auth::change_auth(status, auth::STATUS_NONE); } else if ((in_root_users == true || in_root_groups == true) && this->authstat->get_uid() == 0) { status = auth::change_auth(status, auth::STATUS_NONE); } else if (in_users == true || in_groups == true) // Auth required if not in root group { status = auth::change_auth(status, auth::STATUS_USER); } else // Not in any groups { if (this->authstat->get_ruid() == 0) status = auth::change_auth(status, auth::STATUS_USER); else status = auth::change_auth(status, auth::STATUS_FAIL); } return status; } auth::status session::get_auth_status () const { assert(!this->chroots.empty()); if (this->config.get() == 0) return auth::STATUS_FAIL; /* * Note that the root user can't escape authentication. This is * because pam_rootok.so should be used in the PAM configuration if * root should automatically be granted access. The only exception * is that the root group doesn't need to be added to the groups or * root groups lists. */ auth::status status = auth::STATUS_NONE; /** @todo Use set difference rather than iteration and * is_group_member. */ for (string_list::const_iterator cur = this->chroots.begin(); cur != this->chroots.end(); ++cur) { const chroot::ptr chroot = this->config->find_alias(*cur); if (!chroot) // Should never happen, but cater for it anyway. { error e(*cur, CHROOT_ALIAS); log_exception_warning(e); status = auth::change_auth(status, auth::STATUS_FAIL); } status = auth::change_auth(status, get_chroot_auth_status(status, chroot)); } return status; } void session::run () { try { this->authstat->start(); this->authstat->authenticate(get_auth_status()); this->authstat->setupenv(); this->authstat->account(); try { this->authstat->cred_establish(); run_impl(); /* The session is now finished, either successfully or not. All PAM operations are now for cleanup and shutdown, and we must clean up whether or not errors were raised at any previous point. This means only the first error is reported back to the user. */ /* Don't cope with failure, since we are now already bailing out, and an error may already have been raised */ } catch (auth::error const& e) { try { this->authstat->cred_delete(); } catch (auth::error const& discard) { } throw; } this->authstat->cred_delete(); } catch (auth::error const& e) { try { /* Don't cope with failure, since we are now already bailing out, and an error may already have been raised */ this->authstat->stop(); } catch (auth::error const& discard) { } throw; } this->authstat->stop(); } void session::run_impl () { assert(this->config.get() != 0); assert(!this->chroots.empty()); try { sighup_called = false; set_sighup_handler(); sigterm_called = false; set_sigterm_handler(); for (string_list::const_iterator cur = this->chroots.begin(); cur != this->chroots.end(); ++cur) { log_debug(DEBUG_NOTICE) << format("Running session in %1% chroot:") % *cur << endl; const chroot::ptr ch = this->config->find_alias(*cur); if (!ch) // Should never happen, but cater for it anyway. throw error(*cur, CHROOT_UNKNOWN); // For now, use a copy of the chroot; if we create a session // later, we will replace it. chroot::ptr chroot(ch->clone()); assert(chroot); /* Create a session using randomly-generated session ID. */ if (ch->get_session_flags() & chroot::SESSION_CREATE) { chroot_facet_session_clonable::ptr session_clonable (ch->get_facet()); assert(session_clonable); std::string new_session_id; if (!get_session_id().empty()) { new_session_id = get_session_id(); } else { new_session_id = ch->get_name() + '-' + unique_identifier(); } // Replace clone of chroot with cloned session. bool in_users = false; bool in_root_users = false; bool in_groups = false; bool in_root_groups = false; get_chroot_membership(chroot, in_users, in_root_users, in_groups, in_root_groups); chroot = ch->clone_session(new_session_id, this->authstat->get_ruser(), (in_root_users || in_root_groups)); chroot_facet_session::ptr session (chroot->get_facet()); assert(session); assert(chroot->get_active()); } assert(chroot); // Following authentication success, default child status to // success so that operations such as beginning, ending and // recovering sessions will return success unless an // exception is thrown. this->child_status = EXIT_SUCCESS; try { /* Run setup-start chroot setup scripts. */ setup_chroot(chroot, chroot::SETUP_START); if (this->session_operation == OPERATION_BEGIN) { cout << chroot->get_session_id() << endl; } /* Run recover scripts. */ setup_chroot(chroot, chroot::SETUP_RECOVER); /* Run session if setup succeeded. */ if (this->session_operation == OPERATION_AUTOMATIC || this->session_operation == OPERATION_RUN) { try { this->authstat->open_session(); save_termios(); run_chroot(chroot); } catch (std::runtime_error const& e) { log_debug(DEBUG_WARNING) << "Chroot session failed" << endl; restore_termios(); this->authstat->close_session(); throw; } restore_termios(); this->authstat->close_session(); } } catch (error const& e) { log_debug(DEBUG_WARNING) << "Chroot setup scripts, exec scripts or session failed" << endl; try { setup_chroot(chroot, chroot::SETUP_STOP); } catch (error const& discard) { log_debug(DEBUG_WARNING) << "Chroot setup scripts failed during stop" << endl; } throw; } /* Run setup-stop chroot setup scripts whether or not there was an error. */ setup_chroot(chroot, chroot::SETUP_STOP); } } catch (error const& e) { clear_sigterm_handler(); clear_sighup_handler(); /* If a command was not run, but something failed, the exit status still needs setting. */ if (this->child_status == 0) this->child_status = EXIT_FAILURE; throw; } clear_sigterm_handler(); clear_sighup_handler(); } string_list session::get_login_directories () const { string_list ret; std::string const& wd(this->authstat->get_wd()); if (!wd.empty()) { // Set specified working directory. ret.push_back(wd); } else { // Set current working directory. ret.push_back(this->cwd); // Set $HOME. environment env = this->authstat->get_auth_environment(); std::string home; if (env.get("HOME", home) && std::find(ret.begin(), ret.end(), home) == ret.end()) ret.push_back(home); // Set passwd home. if (std::find(ret.begin(), ret.end(), this->authstat->get_home()) == ret.end()) ret.push_back(this->authstat->get_home()); // Final fallback to root. if (std::find(ret.begin(), ret.end(), "/") == ret.end()) ret.push_back("/"); } return ret; } string_list session::get_command_directories () const { string_list ret; std::string const& wd(this->authstat->get_wd()); if (!wd.empty()) // Set specified working directory. ret.push_back(wd); else // Set current working directory. ret.push_back(this->cwd); return ret; } std::string session::get_shell () const { assert (!this->authstat->get_shell().empty()); std::string shell = this->authstat->get_shell(); try { stat(shell).check(); } catch (std::runtime_error const& e) { error e1(shell, SHELL, e.what()); log_exception_warning(e1); if (shell != "/bin/sh") { shell = "/bin/sh"; error e2(SHELL_FB, shell); log_exception_warning(e2); } } return shell; } void session::get_command (sbuild::chroot::ptr& session_chroot, std::string& file, string_list& command) const { /* Run login shell */ if (command.empty() || command[0].empty()) // No command get_login_command(session_chroot, file, command); else get_user_command(session_chroot, file, command); } void session::get_login_command (sbuild::chroot::ptr& session_chroot, std::string& file, string_list& command) const { command.clear(); std::string shell = get_shell(); file = shell; if (this->authstat->get_environment().empty() && session_chroot->get_command_prefix().empty()) // Not keeping environment and can setup argv correctly; login shell { std::string shellbase = basename(shell); std::string loginshell = "-" + shellbase; command.push_back(loginshell); log_debug(DEBUG_NOTICE) << format("Running login shell: %1%") % shell << endl; if (this->authstat->get_uid() == 0 || this->authstat->get_ruid() != this->authstat->get_uid()) syslog(LOG_USER|LOG_NOTICE, "[%s chroot] (%s->%s) Running login shell: '%s'", session_chroot->get_name().c_str(), this->authstat->get_ruser().c_str(), this->authstat->get_user().c_str(), shell.c_str()); } else { command.push_back(shell); log_debug(DEBUG_NOTICE) << format("Running shell: %1%") % shell << endl; if (this->authstat->get_uid() == 0 || this->authstat->get_ruid() != this->authstat->get_uid()) syslog(LOG_USER|LOG_NOTICE, "[%s chroot] (%s->%s) Running shell: '%s'", session_chroot->get_name().c_str(), this->authstat->get_ruser().c_str(), this->authstat->get_user().c_str(), shell.c_str()); } if (this->authstat->get_verbosity() == auth::VERBOSITY_VERBOSE) { std::string format_string; if (this->authstat->get_ruid() == this->authstat->get_uid()) { if (this->authstat->get_environment().empty() && session_chroot->get_command_prefix().empty()) // TRANSLATORS: %1% = chroot name // TRANSLATORS: %4% = command format_string = _("[%1% chroot] Running login shell: '%4%'"); else // TRANSLATORS: %1% = chroot name // TRANSLATORS: %4% = command format_string = _("[%1% chroot] Running shell: '%4%'"); } else { if (this->authstat->get_environment().empty() && session_chroot->get_command_prefix().empty()) // TRANSLATORS: %1% = chroot name // TRANSLATORS: %2% = user name // TRANSLATORS: %3% = user name // TRANSLATORS: %4% = command // TRANSLATORS: Please translate "->" as a right arrow, e.g. U+2192 format_string = _("[%1% chroot] (%2%->%3%) Running login shell: '%4%'"); else // TRANSLATORS: %1% = chroot name // TRANSLATORS: %2% = user name // TRANSLATORS: %3% = user name // TRANSLATORS: %4% = command // TRANSLATORS: Please translate "->" as a right arrow, e.g. U+2192 format_string = _("[%1% chroot] (%2%->%3%) Running shell: '%4%'"); } format fmt(format_string); fmt % session_chroot->get_name() % this->authstat->get_ruser() % this->authstat->get_user() % shell; log_info() << fmt << endl; } } void session::get_user_command (sbuild::chroot::ptr& session_chroot, std::string& file, string_list& command) const { /* Search for program in path. */ environment env = this->authstat->get_auth_environment(); std::string path; if (!env.get("PATH", path)) path.clear(); file = find_program_in_path(command[0], path, ""); if (file.empty()) file = command[0]; std::string commandstring = string_list_to_string(command, " "); log_debug(DEBUG_NOTICE) << format("Running command: %1%") % commandstring << endl; if (this->authstat->get_uid() == 0 || this->authstat->get_ruid() != this->authstat->get_uid()) syslog(LOG_USER|LOG_NOTICE, "[%s chroot] (%s->%s) Running command: \"%s\"", session_chroot->get_name().c_str(), this->authstat->get_ruser().c_str(), this->authstat->get_user().c_str(), commandstring.c_str()); if (this->authstat->get_verbosity() == auth::VERBOSITY_VERBOSE) { std::string format_string; if (this->authstat->get_ruid() == this->authstat->get_uid()) // TRANSLATORS: %1% = chroot name // TRANSLATORS: %4% = command format_string = _("[%1% chroot] Running command: \"%4%\""); else // TRANSLATORS: %1% = chroot name // TRANSLATORS: %2% = user name // TRANSLATORS: %3% = user name // TRANSLATORS: %4% = command // TRANSLATORS: Please translate "->" as a right arrow, e.g. U+2192 format_string = (_("[%1% chroot] (%2%->%3%) Running command: \"%4%\"")); format fmt(format_string); fmt % session_chroot->get_name() % this->authstat->get_ruser() % this->authstat->get_user() % commandstring; log_info() << fmt << endl; } } void session::setup_chroot (sbuild::chroot::ptr& session_chroot, sbuild::chroot::setup_type setup_type) { assert(!session_chroot->get_name().empty()); log_debug(DEBUG_INFO) << format("setup_chroot: chroot=%1%, setup_type=%2%, chroot_status=%3%, lock_status=%4%") % session_chroot->get_name() % setup_type % chroot_status % lock_status << std::endl; if (!((this->session_operation == OPERATION_BEGIN && setup_type == chroot::SETUP_START) || (this->session_operation == OPERATION_RECOVER && setup_type == chroot::SETUP_RECOVER) || (this->session_operation == OPERATION_END && setup_type == chroot::SETUP_STOP) || (this->session_operation == OPERATION_AUTOMATIC && (setup_type == chroot::SETUP_START || setup_type == chroot::SETUP_STOP)))) return; // Don't clean up chroot on a lock failure--it's actually in use. if (this->lock_status == false) return; if ((setup_type == chroot::SETUP_START || setup_type == chroot::SETUP_RECOVER || setup_type == chroot::SETUP_STOP) && session_chroot->get_run_setup_scripts() == false) return; if (setup_type == chroot::SETUP_START) this->chroot_status = true; try { session_chroot->lock(setup_type); } catch (chroot::error const& e) { this->chroot_status = false; this->lock_status = false; try { // Release lock, which also removes session metadata. session_chroot->unlock(setup_type, 0); } catch (chroot::error const& ignore) { } throw error(session_chroot->get_name(), CHROOT_LOCK, e); } std::string setup_type_string; if (setup_type == chroot::SETUP_START) setup_type_string = "setup-start"; else if (setup_type == chroot::SETUP_RECOVER) setup_type_string = "setup-recover"; else if (setup_type == chroot::SETUP_STOP) setup_type_string = "setup-stop"; std::string chroot_status_string; if (this->chroot_status) chroot_status_string = "ok"; else chroot_status_string = "fail"; string_list arg_list; arg_list.push_back(setup_type_string); arg_list.push_back(chroot_status_string); /* Get a complete list of environment variables to set. We need to query the chroot here, since this can vary depending upon the chroot type. */ environment env; session_chroot->setup_env(env); env.add("AUTH_USER", this->authstat->get_user()); env.add("AUTH_RUSER", this->authstat->get_ruser()); env.add("AUTH_RGROUP", this->authstat->get_rgroup()); env.add("AUTH_UID", this->authstat->get_uid()); env.add("AUTH_GID", this->authstat->get_gid()); env.add("AUTH_RUID", this->authstat->get_ruid()); env.add("AUTH_RGID", this->authstat->get_rgid()); env.add("AUTH_HOME", this->authstat->get_home()); env.add("AUTH_SHELL", this->authstat->get_shell()); { const char *verbosity = 0; switch (this->authstat->get_verbosity()) { case auth::VERBOSITY_QUIET: verbosity = "quiet"; break; case auth::VERBOSITY_NORMAL: verbosity = "normal"; break; case auth::VERBOSITY_VERBOSE: verbosity = "verbose"; break; default: log_debug(DEBUG_CRITICAL) << format("Invalid verbosity level: %1%, falling back to 'normal'") % static_cast(this->authstat->get_verbosity()) << endl; verbosity = "normal"; break; } env.add("AUTH_VERBOSITY", verbosity); } env.add("MOUNT_DIR", SCHROOT_MOUNT_DIR); env.add("LIBEXEC_DIR", SCHROOT_LIBEXEC_DIR); env.add("PID", getpid()); env.add("SESSION_ID", session_chroot->get_session_id()); run_parts rp(SCHROOT_CONF_SETUP_D, true, true, 022); rp.set_reverse(setup_type == chroot::SETUP_STOP); rp.set_verbose(this->authstat->get_verbosity() == auth::VERBOSITY_VERBOSE); log_debug(DEBUG_INFO) << rp << std::endl; int exit_status = 0; pid_t pid; if ((pid = fork()) == -1) { this->chroot_status = false; throw error(session_chroot->get_name(), CHILD_FORK, strerror(errno)); } else if (pid == 0) { try { // The setup scripts don't use our syslog fd. closelog(); chdir("/"); /* This is required to ensure the scripts run with uid=0 and gid=0, otherwise setuid programs such as mount(8) will fail. This should always succeed, because our euid=0 and egid=0.*/ setuid(0); setgid(0); initgroups("root", 0); int status = rp.run(arg_list, env); _exit (status); } catch (std::exception const& e) { sbuild::log_exception_error(e); } catch (...) { sbuild::log_error() << _("An unknown exception occurred") << std::endl; } _exit(EXIT_FAILURE); } else { wait_for_child(pid, exit_status); } try { session_chroot->unlock(setup_type, exit_status); } catch (chroot::error const& e) { this->chroot_status = false; this->lock_status = false; throw error(session_chroot->get_name(), CHROOT_UNLOCK, e); } if (exit_status != 0) { this->chroot_status = false; format fmt(_("stage=%1%")); fmt % setup_type_string; throw error(session_chroot->get_name(), CHROOT_SETUP, fmt.str()); } } void session::run_child (sbuild::chroot::ptr& session_chroot) { assert(!session_chroot->get_name().empty()); assert(!this->authstat->get_user().empty()); assert(!get_shell().empty()); assert(this->authstat->is_initialised()); // PAM must be initialised // Store before chroot call. this->cwd = sbuild::getcwd(); log_debug(DEBUG_INFO) << "CWD=" << this->cwd << std::endl; std::string location(session_chroot->get_path()); log_debug(DEBUG_INFO) << "location=" << location << std::endl; /* Set group ID and supplementary groups */ if (setgid (this->authstat->get_gid())) throw error(this->authstat->get_gid(), GROUP_SET, strerror(errno)); log_debug(DEBUG_NOTICE) << "Set GID=" << this->authstat->get_gid() << std::endl; if (initgroups (this->authstat->get_user().c_str(), this->authstat->get_gid())) throw error(GROUP_SET_SUP, strerror(errno)); log_debug(DEBUG_NOTICE) << "Set supplementary groups" << std::endl; /* Set the process execution domain. */ /* Will throw on failure. */ chroot_facet_personality::const_ptr pfac = session_chroot->get_facet(); if (pfac) { pfac->get_persona().set(); log_debug(DEBUG_NOTICE) << "Set personality=" << pfac->get_persona() << std::endl; } else { log_debug(DEBUG_NOTICE) << "Personality support unavailable" << std::endl; } /* Enter the chroot */ if (chdir (location.c_str())) throw error(location, CHDIR, strerror(errno)); log_debug(DEBUG_NOTICE) << "Changed directory to " << location << std::endl; if (::chroot (location.c_str())) throw error(location, CHROOT, strerror(errno)); log_debug(DEBUG_NOTICE) << "Changed root to " << location << std::endl; /* Set uid and check we are not still root */ if (setuid (this->authstat->get_uid())) throw error(this->authstat->get_uid(), USER_SET, strerror(errno)); log_debug(DEBUG_NOTICE) << "Set UID=" << this->authstat->get_uid() << std::endl; if (!setuid (0) && this->authstat->get_uid()) throw error(ROOT_DROP); if (this->authstat->get_uid()) log_debug(DEBUG_NOTICE) << "Dropped root privileges" << std::endl; std::string file; string_list command(this->authstat->get_command()); string_list dlist; if (command.empty() || command[0].empty()) // No command dlist = get_login_directories(); else dlist = get_command_directories(); log_debug(DEBUG_INFO) << format("Directory fallbacks: %1%") % string_list_to_string(dlist, ", ") << endl; /* Attempt to chdir to current directory. */ for (string_list::const_iterator dpos = dlist.begin(); dpos != dlist.end(); ++dpos) { if (chdir ((*dpos).c_str()) < 0) { error e(*dpos, CHDIR, strerror(errno)); if (dpos + 1 == dlist.end()) throw e; else log_exception_warning(e); } else { log_debug(DEBUG_NOTICE) << "Changed directory to " << *dpos << std::endl; if (dpos != dlist.begin()) { error e(CHDIR_FB, *dpos); log_exception_warning(e); } break; } } /* Fix up the command for exec. */ get_command(session_chroot, file, command); log_debug(DEBUG_NOTICE) << "command=" << string_list_to_string(command, ", ") << std::endl; /* Set up environment */ environment env; env.set_filter(session_chroot->get_environment_filter()); env += this->authstat->get_auth_environment(); // Add equivalents to sudo's SUDO_USER, SUDO_UID, SUDO_GID, and // SUDO_COMMAND. env.add(std::make_pair("SCHROOT_COMMAND", string_list_to_string(command, " "))); env.add(std::make_pair("SCHROOT_USER", this->authstat->get_ruser())); env.add(std::make_pair("SCHROOT_GROUP", this->authstat->get_rgroup())); env.add("SCHROOT_UID", this->authstat->get_ruid()); env.add("SCHROOT_GID", this->authstat->get_rgid()); // Add session ID. env.add("SCHROOT_SESSION_ID", session_chroot->get_session_id()); log_debug(DEBUG_INFO) << "Set environment:\n" << env; // The user's command does not use our syslog fd. closelog(); // Add command prefix. string_list full_command(session_chroot->get_command_prefix()); if (full_command.size() > 0) { std::string path; if (!env.get("PATH", path)) path.clear(); file = find_program_in_path(full_command[0], path, ""); } for (string_list::const_iterator pos = command.begin(); pos != command.end(); ++pos) full_command.push_back(*pos); /* Execute */ if (exec (file, full_command, env)) throw error(file, EXEC, strerror(errno)); /* This should never be reached */ _exit(EXIT_FAILURE); } void session::wait_for_child (pid_t pid, int& child_status) { child_status = EXIT_FAILURE; // Default exit status int status; bool child_killed = false; while (1) { if ((sighup_called || sigterm_called) && !child_killed) { if (sighup_called) { error e(SIGNAL_CATCH, strsignal(SIGHUP), _("terminating immediately")); log_exception_error(e); kill(pid, SIGHUP); } else // SIGTERM { error e(SIGNAL_CATCH, strsignal(SIGTERM), _("terminating immediately")); log_exception_error(e); kill(pid, SIGTERM); } this->chroot_status = false; child_killed = true; } if (waitpid(pid, &status, 0) == -1) { if (errno == EINTR && (sighup_called || sigterm_called)) continue; // Kill child and wait again. else throw error(CHILD_WAIT, strerror(errno)); } else if (sighup_called) { sighup_called = false; throw error(SIGNAL_CATCH, strsignal(SIGHUP)); } else if (sigterm_called) { sigterm_called = false; throw error(SIGNAL_CATCH, strsignal(SIGTERM)); } else break; } if (!WIFEXITED(status)) { if (WIFSIGNALED(status)) throw error(CHILD_SIGNAL, strsignal(WTERMSIG(status))); else if (WCOREDUMP(status)) throw error(CHILD_CORE); else throw error(CHILD_FAIL); } child_status = WEXITSTATUS(status); } void session::run_chroot (sbuild::chroot::ptr& session_chroot) { assert(!session_chroot->get_name().empty()); pid_t pid; if ((pid = fork()) == -1) { throw error(CHILD_FORK, strerror(errno)); } else if (pid == 0) { #ifdef SBUILD_DEBUG while (child_wait) ; #endif try { run_child(session_chroot); } catch (std::runtime_error const& e) { log_exception_error(e); } catch (...) { sbuild::log_error() << _("An unknown exception occurred") << std::endl; } _exit (EXIT_FAILURE); } else { wait_for_child(pid, this->child_status); } } void session::set_sighup_handler () { set_signal_handler(SIGHUP, &this->saved_sighup_signal, sighup_handler); } void session::clear_sighup_handler () { clear_signal_handler(SIGHUP, &this->saved_sighup_signal); } void session::set_sigterm_handler () { set_signal_handler(SIGTERM, &this->saved_sigterm_signal, sigterm_handler); } void session::clear_sigterm_handler () { clear_signal_handler(SIGTERM, &this->saved_sigterm_signal); } void session::set_signal_handler (int signal, struct sigaction *saved_signal, void (*handler)(int)) { struct sigaction new_sa; sigemptyset(&new_sa.sa_mask); new_sa.sa_flags = 0; new_sa.sa_handler = handler; if (sigaction(signal, &new_sa, saved_signal) != 0) throw error(SIGNAL_SET, strsignal(signal), strerror(errno)); } void session::clear_signal_handler (int signal, struct sigaction *saved_signal) { /* Restore original handler */ sigaction (signal, saved_signal, 0); }