mirror of
https://github.com/facebook/sapling.git
synced 2024-10-10 00:45:18 +03:00
c55781c666
Summary: Move the `UserInfo` code from `fuse/privhelper` to `utils`, and also unify the POSIX and Windows implementations of this class. This code was originally put in the `fuse/privhelper` directory since it was written at the same time as the privhelper. However, it's really a lower-level library that doesn't depend on FUSE or any of the other code in the `fuse/` subdirectory. Reviewed By: wez Differential Revision: D21296594 fbshipit-source-id: f58682f6ce86bba0328472c491bb4c0dc3370319
337 lines
9.9 KiB
C++
337 lines
9.9 KiB
C++
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This software may be used and distributed according to the terms of the
|
|
* GNU General Public License version 2.
|
|
*/
|
|
|
|
#include "eden/fs/utils/UserInfo.h"
|
|
|
|
#ifndef _WIN32
|
|
#include <grp.h>
|
|
#include <pwd.h>
|
|
#ifdef __linux__
|
|
#include <sys/prctl.h>
|
|
#endif // __linux__
|
|
#ifdef EDEN_HAVE_SELINUX
|
|
#include <selinux/selinux.h> // @manual
|
|
#endif // EDEN_HAVE_SELINUX
|
|
#else // !_WIN32
|
|
#include <Lmcons.h> // @manual
|
|
#include <userenv.h> // @manual
|
|
#include "eden/fs/win/utils/Handle.h" // @manual
|
|
#include "eden/fs/win/utils/WinError.h" // @manual
|
|
#endif // _WIN32
|
|
|
|
#include <vector>
|
|
|
|
#include <folly/Exception.h>
|
|
#include <folly/logging/xlog.h>
|
|
|
|
using folly::checkUnixError;
|
|
using folly::throwSystemError;
|
|
using std::string;
|
|
|
|
namespace facebook {
|
|
namespace eden {
|
|
|
|
#ifndef _WIN32
|
|
struct UserInfo::PasswdEntry {
|
|
struct passwd pwd;
|
|
std::vector<char> buf;
|
|
};
|
|
|
|
static void dropToBasicSELinuxPrivileges() {
|
|
#ifdef EDEN_HAVE_SELINUX
|
|
const char* baseContext = "user_u:base_r:base_t";
|
|
|
|
XLOG(DBG2) << "Dropping SELinux context..." << [&] {
|
|
char* con;
|
|
if (0 == getcon(&con)) {
|
|
SCOPE_SUCCESS {
|
|
freecon(con);
|
|
};
|
|
return " prior context was: " + string(con);
|
|
}
|
|
return string();
|
|
}();
|
|
|
|
// Drop to basic user SELinux privileges.
|
|
// This is required in order to gdb into edenfs without sudo.
|
|
if (setcon(const_cast<char*>(baseContext))) {
|
|
XLOG(DBG3) << "setcon() failed when dropping SELinux context";
|
|
}
|
|
#endif // EDEN_HAVE_SELINUX
|
|
}
|
|
|
|
void UserInfo::dropPrivileges() {
|
|
// If we are not privileged, there is nothing to do.
|
|
// Return early in this case; otherwise the initgroups() call below
|
|
// is likely to fail.
|
|
if (uid_ == getuid() && uid_ == geteuid() && gid_ == getgid() &&
|
|
gid_ == getegid()) {
|
|
return;
|
|
}
|
|
|
|
// Configure the correct supplementary groups
|
|
auto rc = initgroups(username_.c_str(), gid_);
|
|
checkUnixError(rc, "failed to set supplementary groups");
|
|
// Drop to the correct primary group
|
|
rc = setregid(gid_, gid_);
|
|
checkUnixError(rc, "failed to drop group privileges");
|
|
// Drop to the correct user ID
|
|
rc = setreuid(uid_, uid_);
|
|
checkUnixError(rc, "failed to drop user privileges");
|
|
|
|
#ifdef __linux__
|
|
// Per PR_SET_DUMPABLE's documentation in ptrace(2), the dumpable bit is set
|
|
// to 0 on any call to setregid or setreuid. Since we've dropped privileges,
|
|
// reset the dumpable bit to 1 so gdb can attach to Eden without running as
|
|
// root. This also means that edenfs can produce core dumps.
|
|
rc = prctl(PR_SET_DUMPABLE, 1, 0, 0, 0);
|
|
checkUnixError(rc, "failed to mark process dumpable");
|
|
#endif
|
|
|
|
// If we started under sudo, update the environment to restore $USER
|
|
// and drop the $SUDO_* variables.
|
|
restoreEnvironmentAfterSudo();
|
|
|
|
dropToBasicSELinuxPrivileges();
|
|
}
|
|
|
|
void UserInfo::restoreEnvironmentAfterSudo() {
|
|
// Skip updating the environment if we do not appear to have
|
|
// been started by sudo.
|
|
//
|
|
// Updating the environment is not thread-safe, so let's avoid it if we can.
|
|
// Ideally we should always be dropping privileges before any other threads
|
|
// exist that might be checking environment variables, but it seems better to
|
|
// avoid updating it if possible.
|
|
if (getenv("SUDO_UID") == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// Update the $USER environment variable. This is important so that any
|
|
// subprocesses we spawn (such as "hg debugedenimporthelper") see the correct
|
|
// $USER value.
|
|
setenv("USER", username_.c_str(), 1);
|
|
// sudo also sets the USERNAME and LOGNAME environment variables.
|
|
// Update these as well.
|
|
setenv("USERNAME", username_.c_str(), 1);
|
|
setenv("LOGNAME", username_.c_str(), 1);
|
|
|
|
// Clear out the other SUDO_* variables for good measure.
|
|
unsetenv("SUDO_USER");
|
|
unsetenv("SUDO_UID");
|
|
unsetenv("SUDO_GID");
|
|
unsetenv("SUDO_COMMAND");
|
|
}
|
|
|
|
EffectiveUserScope::EffectiveUserScope(const UserInfo& userInfo)
|
|
: ruid_(getuid()), euid_(geteuid()), rgid_(getgid()), egid_(getegid()) {
|
|
checkUnixError(
|
|
setregid(userInfo.getGid(), userInfo.getGid()),
|
|
"setregid() failed in EffectiveUserScope()");
|
|
checkUnixError(
|
|
setreuid(0, userInfo.getUid()),
|
|
"setreuid() failed in EffectiveUserScope()");
|
|
}
|
|
|
|
EffectiveUserScope::~EffectiveUserScope() {
|
|
checkUnixError(
|
|
setreuid(ruid_, euid_), "setreuid() failed in ~EffectiveUserScope()");
|
|
checkUnixError(
|
|
setregid(rgid_, egid_), "setregid() failed in ~EffectiveUserScope()");
|
|
}
|
|
|
|
UserInfo::PasswdEntry UserInfo::getPasswdUid(uid_t uid) {
|
|
static constexpr size_t initialBufSize = 1024;
|
|
static constexpr size_t maxBufSize = 8192;
|
|
PasswdEntry pwd;
|
|
pwd.buf.resize(initialBufSize);
|
|
|
|
struct passwd* result;
|
|
while (true) {
|
|
const auto errnum =
|
|
getpwuid_r(uid, &pwd.pwd, pwd.buf.data(), pwd.buf.size(), &result);
|
|
if (errnum == 0) {
|
|
break;
|
|
} else if (errnum == ERANGE && pwd.buf.size() < maxBufSize) {
|
|
// Retry with a bigger buffer
|
|
pwd.buf.resize(pwd.buf.size() * 2);
|
|
continue;
|
|
} else {
|
|
throwSystemError("unable to look up user information for UID ", uid);
|
|
}
|
|
}
|
|
if (result == nullptr) {
|
|
// No user info present for this UID.
|
|
throwSystemError("no passwd entry found for UID ", uid);
|
|
}
|
|
|
|
return pwd;
|
|
}
|
|
|
|
bool UserInfo::initFromSudo() {
|
|
// If SUDO_UID is not set, return false indicating we could not
|
|
// find sudo-based identity information.
|
|
const auto sudoUid = getenv("SUDO_UID");
|
|
if (sudoUid == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
// Throw an exception if SUDO_GID or SUDI_USER is not set, or if we cannot
|
|
// parse them below. We want to fail hard if we have SUDO_UID but we can't
|
|
// use it for some reason. We don't want to fall back to running as root in
|
|
// this case.
|
|
const auto sudoGid = getenv("SUDO_GID");
|
|
if (sudoGid == nullptr) {
|
|
throw std::runtime_error("SUDO_UID set without SUDO_GID");
|
|
}
|
|
const auto sudoUser = getenv("SUDO_USER");
|
|
if (sudoUser == nullptr) {
|
|
throw std::runtime_error("SUDO_UID set without SUDO_USER");
|
|
}
|
|
|
|
try {
|
|
uid_ = folly::to<uid_t>(sudoUid);
|
|
} catch (const std::range_error&) {
|
|
throw std::runtime_error(string{"invalid value for SUDO_UID: "} + sudoUid);
|
|
}
|
|
try {
|
|
gid_ = folly::to<gid_t>(sudoGid);
|
|
} catch (const std::range_error&) {
|
|
throw std::runtime_error(string{"invalid value for SUDO_GID: "} + sudoGid);
|
|
}
|
|
|
|
username_ = sudoUser;
|
|
initHomedir();
|
|
return true;
|
|
}
|
|
|
|
void UserInfo::initFromNonRoot(uid_t uid) {
|
|
uid_ = uid;
|
|
gid_ = getgid();
|
|
|
|
// Always look up the username from the UID.
|
|
// We cannot trust the USER environment variable--the user could have set
|
|
// it to anything.
|
|
auto pwd = getPasswdUid(uid_);
|
|
username_ = pwd.pwd.pw_name;
|
|
|
|
initHomedir(&pwd);
|
|
}
|
|
|
|
void UserInfo::initHomedir(PasswdEntry* pwd) {
|
|
// We do trust the $HOME environment variable if it is set.
|
|
// This does not need to be distrusted for security reasons--we can use any
|
|
// arbitrary directory the user wants as long as they have read/write access
|
|
// to it. We only access it after dropping privileges.
|
|
//
|
|
// Note that we intentionally use canonicalPath() rather than realpath()
|
|
// here. realpath() will perform symlink resolution. initHomedir() will
|
|
// generally be run before we have dropped privileges, and we do not want to
|
|
// try traversing symlinks that the user may not actually have permissions to
|
|
// resolve.
|
|
const auto homeEnv = getenv("HOME");
|
|
if (homeEnv != nullptr) {
|
|
homeDirectory_ = canonicalPath(homeEnv);
|
|
return;
|
|
}
|
|
|
|
PasswdEntry locallyLookedUp;
|
|
if (!pwd) {
|
|
locallyLookedUp = getPasswdUid(uid_);
|
|
pwd = &locallyLookedUp;
|
|
}
|
|
|
|
if (pwd && pwd->pwd.pw_dir) {
|
|
homeDirectory_ = canonicalPath(pwd->pwd.pw_dir);
|
|
return;
|
|
}
|
|
|
|
// Fall back to the root directory if all else fails
|
|
homeDirectory_ = AbsolutePath{"/"};
|
|
}
|
|
|
|
UserInfo UserInfo::lookup() {
|
|
UserInfo info;
|
|
// First check the real UID. If it is non-root, use that.
|
|
// This happens if our binary is setuid root and invoked by a non-root user.
|
|
const auto uid = getuid();
|
|
if (uid != 0) {
|
|
info.initFromNonRoot(uid);
|
|
return info;
|
|
}
|
|
|
|
// If we are still here, our real UID is 0.
|
|
// Check the SUDO_* environment variables in case we are running under sudo.
|
|
if (info.initFromSudo()) {
|
|
return info;
|
|
}
|
|
|
|
// If we are still here, we are actually running as root and could not find
|
|
// non-root privileges to drop to.
|
|
info.uid_ = uid;
|
|
info.gid_ = getgid();
|
|
auto pwd = getPasswdUid(info.uid_);
|
|
info.username_ = pwd.pwd.pw_name;
|
|
info.initHomedir(&pwd);
|
|
return info;
|
|
}
|
|
#else // _WIN32
|
|
UserInfo UserInfo::lookup() {
|
|
UserInfo info;
|
|
|
|
info.username_.resize(UNLEN);
|
|
DWORD size = folly::to_narrow(info.username_.size() + 1);
|
|
if (!GetUserNameA(info.username_.data(), &size)) {
|
|
throw makeWin32ErrorExplicit(GetLastError(), "Failed to get the user name");
|
|
}
|
|
info.username_.resize(size - 1);
|
|
|
|
TokenHandle tokenHandle;
|
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, tokenHandle.set())) {
|
|
throw makeWin32ErrorExplicit(
|
|
GetLastError(), "Failed to get the process token");
|
|
}
|
|
|
|
// The profile path could be of any arbitrary length, so if we failed to get
|
|
// with error ERROR_INSUFFICIENT_BUFFER then we retry with the right size
|
|
// buffer.
|
|
|
|
size = MAX_PATH;
|
|
string profile(size - 1, 0);
|
|
bool retry = false;
|
|
|
|
if (!GetUserProfileDirectoryA(tokenHandle.get(), profile.data(), &size)) {
|
|
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
|
|
retry = true;
|
|
} else {
|
|
throw makeWin32ErrorExplicit(
|
|
GetLastError(), "Failed to get user profile directory");
|
|
}
|
|
}
|
|
|
|
profile.resize(size - 1);
|
|
if (retry) {
|
|
if (!GetUserProfileDirectoryA(tokenHandle.get(), profile.data(), &size)) {
|
|
throw makeWin32ErrorExplicit(
|
|
GetLastError(), "Failed to get user profile directory");
|
|
}
|
|
profile.resize(size - 1);
|
|
}
|
|
|
|
info.homeDirectory_ = realpath(profile);
|
|
|
|
return info;
|
|
}
|
|
|
|
void UserInfo::dropPrivileges() {
|
|
// EdenFS does not run with elevated privileges on Windows,
|
|
// so there is nothing to do here.
|
|
}
|
|
#endif // _WIN32
|
|
} // namespace eden
|
|
} // namespace facebook
|