blob: b4f0ecace5d258f03bf5620231ad31f379d09651 [file] [log] [blame]
/** @file
A brief file description
@section license License
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "tscore/ink_config.h"
#include "tscore/Diags.h"
#include "tscore/ink_cap.h"
#include "tscore/ink_thread.h"
#include <grp.h>
#if HAVE_SYS_CAPABILITY_H
#include <sys/capability.h>
#endif
#if HAVE_SYS_PRCTL_H
#include <sys/prctl.h>
#endif
// NOTE: Failing to acquire or release privileges is a fatal error. This is because that should never happen
// and if it does, it is likely that some fundamental security assumption has been violated. In that case
// it is dangerous to continue.
#if !TS_USE_POSIX_CAP
ink_mutex ElevateAccess::lock = INK_MUTEX_INIT;
#endif
#define DEBUG_CREDENTIALS(tag) \
do { \
if (is_debug_tag_set(tag)) { \
uid_t uid = -1, euid = -1, suid = -1; \
gid_t gid = -1, egid = -1, sgid = -1; \
getresuid(&uid, &euid, &suid); \
getresgid(&gid, &egid, &sgid); \
Debug(tag, "uid=%ld, gid=%ld, euid=%ld, egid=%ld, suid=%ld, sgid=%ld", static_cast<long>(uid), static_cast<long>(gid), \
static_cast<long>(euid), static_cast<long>(egid), static_cast<long>(suid), static_cast<long>(sgid)); \
} \
} while (0)
#if TS_USE_POSIX_CAP
#define DEBUG_PRIVILEGES(tag) \
do { \
if (is_debug_tag_set(tag)) { \
cap_t caps = cap_get_proc(); \
char *caps_text = cap_to_text(caps, nullptr); \
Debug(tag, "caps='%s', core=%s, death signal=%d, thread=0x%llx", caps_text, is_dumpable(), death_signal(), \
(unsigned long long)pthread_self()); \
cap_free(caps_text); \
cap_free(caps); \
} \
} while (0)
#else /* TS_USE_POSIX_CAP */
#define DEBUG_PRIVILEGES(tag) \
do { \
if (is_debug_tag_set(tag)) { \
Debug(tag, "caps='', core=%s, death signal=%d, thread=0x%llx", is_dumpable(), death_signal(), \
(unsigned long long)pthread_self()); \
} \
} while (0)
#endif /* TS_USE_POSIX_CAP */
#if !HAVE_GETRESUID
static int
getresuid(uid_t *uid, uid_t *euid, uid_t *suid)
{
*uid = getuid();
*euid = geteuid();
return 0;
}
#endif /* !HAVE_GETRESUID */
#if !HAVE_GETRESGID
static int
getresgid(gid_t *gid, gid_t *egid, gid_t *sgid)
{
*gid = getgid();
*egid = getegid();
return 0;
}
#endif /* !HAVE_GETRESGID */
static unsigned
max_passwd_size()
{
#if defined(_SC_GETPW_R_SIZE_MAX)
long val = sysconf(_SC_GETPW_R_SIZE_MAX);
if (val > 0) {
return static_cast<unsigned>(val);
}
#endif
return 4096;
}
static const char *
is_dumpable()
{
#if defined(PR_GET_DUMPABLE)
return (prctl(PR_GET_DUMPABLE) != 1) ? "disabled" : "enabled";
#else
return "unknown";
#endif
}
static int
death_signal()
{
int signum = -1;
#if defined(PR_GET_PDEATHSIG)
prctl(PR_GET_PDEATHSIG, &signum, 0, 0, 0);
#endif
return signum;
}
void
DebugCapabilities(const char *tag)
{
DEBUG_CREDENTIALS(tag);
DEBUG_PRIVILEGES(tag);
}
static void
impersonate(const struct passwd *pwd, ImpersonationLevel level)
{
int deathsig = death_signal();
bool dumpable = false;
DEBUG_CREDENTIALS("privileges");
DEBUG_PRIVILEGES("privileges");
ink_release_assert(pwd != nullptr);
#if defined(PR_GET_DUMPABLE)
dumpable = (prctl(PR_GET_DUMPABLE) == 1);
#endif
// Always repopulate the supplementary group list for the new user.
initgroups(pwd->pw_name, pwd->pw_gid);
switch (level) {
case IMPERSONATE_PERMANENT:
if (setregid(pwd->pw_gid, pwd->pw_gid) != 0) {
Fatal("switching to user %s, failed to set group ID %ld", pwd->pw_name, (long)pwd->pw_gid);
}
if (setreuid(pwd->pw_uid, pwd->pw_uid) != 0) {
Fatal("switching to user %s, failed to set user ID %ld", pwd->pw_name, (long)pwd->pw_uid);
}
break;
case IMPERSONATE_EFFECTIVE:
if (setegid(pwd->pw_gid) != 0) {
Fatal("switching to user %s, failed to set group ID %ld", pwd->pw_name, (long)pwd->pw_gid);
}
if (seteuid(pwd->pw_uid) != 0) {
Fatal("switching to user %s, failed to set effective user ID %ld", pwd->pw_name, (long)pwd->pw_gid);
}
break;
}
// Reset process flags if necessary. Elevating privilege using capabilities does not reset process
// flags, so we don't have to bother with this in elevateFileAccess().
EnableCoreFile(dumpable);
if (deathsig > 0) {
EnableDeathSignal(deathsig);
}
DEBUG_CREDENTIALS("privileges");
DEBUG_PRIVILEGES("privileges");
}
void
ImpersonateUserID(uid_t uid, ImpersonationLevel level)
{
struct passwd *pwd;
struct passwd pbuf;
char buf[max_passwd_size()];
if (getpwuid_r(uid, &pbuf, buf, sizeof(buf), &pwd) != 0) {
Fatal("missing password database entry for UID %ld: %s", (long)uid, strerror(errno));
}
if (pwd == nullptr) {
// Password entry not found ...
Fatal("missing password database entry for UID %ld", (long)uid);
}
impersonate(pwd, level);
}
void
ImpersonateUser(const char *user, ImpersonationLevel level)
{
struct passwd *pwd;
struct passwd pbuf;
char buf[max_passwd_size()];
if (*user == '#') {
// Numeric user notation.
uid_t uid = static_cast<uid_t>(atoi(&user[1]));
if (getpwuid_r(uid, &pbuf, buf, sizeof(buf), &pwd) != 0) {
Fatal("missing password database entry for UID %ld: %s", (long)uid, strerror(errno));
}
} else {
if (getpwnam_r(user, &pbuf, buf, sizeof(buf), &pwd) != 0) {
Fatal("missing password database entry for username '%s': %s", user, strerror(errno));
}
}
if (pwd == nullptr) {
// Password entry not found ...
Fatal("missing password database entry for '%s'", user);
}
impersonate(pwd, level);
}
bool
PreserveCapabilities()
{
int zret = 0;
#if TS_USE_POSIX_CAP
zret = prctl(PR_SET_KEEPCAPS, 1);
#endif
Debug("privileges", "[PreserveCapabilities] zret : %d", zret);
return zret == 0;
}
// Adjust the capabilities to only those needed.
bool
RestrictCapabilities()
{
int zret = 0; // return value.
#if TS_USE_POSIX_CAP
cap_t caps_good = cap_init(); // Start with nothing
cap_t caps_orig = cap_get_proc();
// Capabilities we need.
cap_value_t perm_list[] = {CAP_NET_ADMIN, CAP_NET_BIND_SERVICE, CAP_IPC_LOCK, CAP_DAC_OVERRIDE, CAP_FOWNER};
static int const PERM_CAP_COUNT = sizeof(perm_list) / sizeof(*perm_list);
cap_value_t eff_list[] = {CAP_NET_ADMIN, CAP_NET_BIND_SERVICE, CAP_IPC_LOCK};
static int const EFF_CAP_COUNT = sizeof(eff_list) / sizeof(*eff_list);
// Request capabilities one at a time. If one capability fails
// the rest may succeed. If this scenario does not need that capability
// Must start with the current privileges in case we fail we can get back in
// that is ok.
for (int i = 0; i < PERM_CAP_COUNT; i++) {
cap_t caps = cap_get_proc();
if (cap_set_flag(caps, CAP_PERMITTED, 1, perm_list + i, CAP_SET) < 0) {
} else {
if (cap_set_proc(caps) == -1) { // it failed, back out
Warning("CAP_PERMITTED failed for option %d", i);
} else {
if (cap_set_flag(caps_good, CAP_PERMITTED, 1, perm_list + i, CAP_SET) < 0) {
}
}
}
if (cap_set_proc(caps_orig) < 0) {
ink_release_assert(0);
}
cap_free(caps);
}
for (int i = 0; i < EFF_CAP_COUNT; i++) {
cap_t caps = cap_get_proc();
if (cap_set_flag(caps, CAP_EFFECTIVE, 1, eff_list + i, CAP_SET) < 0) {
} else {
if (cap_set_proc(caps) == -1) { // it failed, back out
Warning("CAP_EFFECTIVE failed for option %d", i);
} else {
if (cap_set_flag(caps_good, CAP_EFFECTIVE, 1, eff_list + i, CAP_SET) < 0) {
}
}
}
if (cap_set_proc(caps_orig) < 0) {
ink_release_assert(0);
}
cap_free(caps);
}
if (cap_set_proc(caps_good) == -1) { // it failed, back out
ink_release_assert(0);
}
for (int i = 0; i < PERM_CAP_COUNT; i++) {
cap_flag_value_t val;
if (cap_get_flag(caps_good, perm_list[i], CAP_PERMITTED, &val) < 0) {
} else {
Warning("CAP_PERMITTED offiset %d is %s", i, val == CAP_SET ? "set" : "unset");
}
}
for (int i = 0; i < EFF_CAP_COUNT; i++) {
cap_flag_value_t val;
if (cap_get_flag(caps_good, eff_list[i], CAP_EFFECTIVE, &val) < 0) {
} else {
Warning("CAP_EFFECTIVE offiset %d is %s", i, val == CAP_SET ? "set" : "unset");
}
}
cap_free(caps_good);
cap_free(caps_orig);
#endif
Debug("privileges", "[RestrictCapabilities] zret : %d", zret);
return zret == 0;
}
bool
EnableCoreFile(bool flag)
{
int zret = 0;
#if defined(PR_SET_DUMPABLE)
int state = flag ? 1 : 0;
if (0 > (zret = prctl(PR_SET_DUMPABLE, state, 0, 0, 0))) {
Warning("Unable to set PR_DUMPABLE : %s", strerror(errno));
} else if (state != prctl(PR_GET_DUMPABLE)) {
zret = ENOSYS; // best guess
Warning("Call to set PR_DUMPABLE was ineffective");
}
#endif // linux check
Debug("privileges", "[EnableCoreFile] zret : %d", zret);
return zret == 0;
}
void
EnableDeathSignal(int signum)
{
(void)signum;
#if defined(PR_SET_PDEATHSIG)
if (prctl(PR_SET_PDEATHSIG, signum, 0, 0, 0) != 0) {
Debug("privileges", "prctl(PR_SET_PDEATHSIG) failed: %s", strerror(errno));
}
#endif
}
int
elevating_open(const char *path, unsigned int flags, unsigned int fperms)
{
int fd = open(path, flags, fperms);
if (fd < 0 && (EPERM == errno || EACCES == errno)) {
ElevateAccess access(ElevateAccess::FILE_PRIVILEGE);
fd = open(path, flags, fperms);
}
return fd;
}
int
elevating_open(const char *path, unsigned int flags)
{
int fd = open(path, flags);
if (fd < 0 && (EPERM == errno || EACCES == errno)) {
ElevateAccess access(ElevateAccess::FILE_PRIVILEGE);
fd = open(path, flags);
}
return fd;
}
FILE *
elevating_fopen(const char *path, const char *mode)
{
FILE *f = fopen(path, mode);
if (nullptr == f && (EPERM == errno || EACCES == errno)) {
ElevateAccess access(ElevateAccess::FILE_PRIVILEGE);
f = fopen(path, mode);
}
return f;
}
int
elevating_chmod(const char *path, int perm)
{
int ret = chmod(path, perm);
if (ret != 0 && (EPERM == errno || EACCES == errno)) {
ElevateAccess access(ElevateAccess::OWNER_PRIVILEGE);
return chmod(path, perm);
}
return ret;
}
int
elevating_stat(const char *path, struct stat *buff)
{
int ret = stat(path, buff);
if (ret != 0 && (EPERM == errno || EACCES == errno)) {
ElevateAccess access(ElevateAccess::FILE_PRIVILEGE);
return stat(path, buff);
}
return ret;
}
#if TS_USE_POSIX_CAP
/** Acquire file access privileges to bypass DAC.
@a level is a mask of the specific file access capabilities to acquire.
*/
void
ElevateAccess::acquirePrivilege(unsigned priv_mask)
{
unsigned cap_count = 0;
cap_value_t cap_list[3];
cap_t new_cap_state;
Debug("privileges", "[acquirePrivilege] level= %x", level);
ink_assert(nullptr == cap_state);
// Some privs aren't checked or used here because they are kept permanently in the
// the capability list. See @a eff_list in @c RestrictCapabilities
// It simplifies things elsewhere to be able to specify them so that the cases for
// POSIX capabilities and user impersonation have the same interface.
if (priv_mask & ElevateAccess::FILE_PRIVILEGE) {
cap_list[cap_count] = CAP_DAC_OVERRIDE;
++cap_count;
}
if (priv_mask & ElevateAccess::TRACE_PRIVILEGE) {
cap_list[cap_count] = CAP_SYS_PTRACE;
++cap_count;
}
if (priv_mask & ElevateAccess::OWNER_PRIVILEGE) {
cap_list[cap_count] = CAP_FOWNER;
++cap_count;
}
ink_release_assert(cap_count <= sizeof(cap_list));
if (cap_count > 0) {
this->cap_state = cap_get_proc(); // save current capabilities
new_cap_state = cap_get_proc(); // and another instance to modify.
cap_set_flag(new_cap_state, CAP_EFFECTIVE, cap_count, cap_list, CAP_SET);
if (cap_set_proc(new_cap_state) != 0) {
Fatal("failed to acquire privileged capabilities: %s", strerror(errno));
}
cap_free(new_cap_state);
elevated = true;
}
}
/** Restore previous capabilities.
*/
void
ElevateAccess::releasePrivilege()
{
Debug("privileges", "[releaseFileAccessCap]");
if (this->cap_state) {
if (cap_set_proc(static_cast<cap_t>(cap_state)) != 0) {
Fatal("failed to restore privileged capabilities: %s", strerror(errno));
}
cap_free(this->cap_state);
cap_state = nullptr;
}
}
#endif
ElevateAccess::ElevateAccess(unsigned lvl)
: saved_uid(geteuid()),
level(lvl)
#if TS_USE_POSIX_CAP
,
cap_state(nullptr)
#endif
{
elevate(level);
#if !TS_USE_POSIX_CAP
DEBUG_CREDENTIALS("privileges");
#endif
DEBUG_PRIVILEGES("privileges");
}
ElevateAccess::~ElevateAccess()
{
if (elevated) {
demote();
#if !TS_USE_POSIX_CAP
DEBUG_CREDENTIALS("privileges");
#endif
DEBUG_PRIVILEGES("privileges");
}
}
void
ElevateAccess::elevate(unsigned priv_mask)
{
#if TS_USE_POSIX_CAP
acquirePrivilege(priv_mask);
#else
if (priv_mask) {
// Since we are setting a process-wide credential, we have to block any other thread
// attempting to elevate until this one demotes.
ink_mutex_acquire(&lock);
ImpersonateUserID(0, IMPERSONATE_EFFECTIVE);
elevated = true;
}
#endif
}
void
ElevateAccess::demote()
{
if (elevated) {
#if TS_USE_POSIX_CAP
releasePrivilege();
#else
ImpersonateUserID(saved_uid, IMPERSONATE_EFFECTIVE);
ink_mutex_release(&lock);
#endif
elevated = false;
}
}