| /** @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; |
| } |
| } |