| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed 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. |
| */ |
| |
| #define LOG_TAG "execns" |
| #include <log/log.h> |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <grp.h> |
| #include <pwd.h> |
| #include <sched.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| |
| #include <string> |
| #include <vector> |
| |
| static bool isTerminal = false; |
| // Print errors to stderr if running from a terminal, otherwise print to logcat |
| // This is useful for debugging from a terminal |
| #define LOGE(...) do { \ |
| if (isTerminal) { \ |
| fprintf(stderr, __VA_ARGS__); \ |
| fprintf(stderr, "\n"); \ |
| } else { \ |
| ALOGE(__VA_ARGS__); \ |
| } \ |
| } while (0) |
| |
| static const char kNetNsDir[] = "/data/vendor/var/run/netns"; |
| |
| class FileDescriptor { |
| public: |
| explicit FileDescriptor(int fd) : mFd(fd) { } |
| FileDescriptor(const FileDescriptor&) = delete; |
| ~FileDescriptor() { |
| if (mFd != -1) { |
| close(mFd); |
| mFd = -1; |
| } |
| } |
| int get() const { return mFd; } |
| FileDescriptor& operator=(const FileDescriptor&) = delete; |
| private: |
| int mFd; |
| }; |
| |
| class File { |
| public: |
| explicit File(FILE* file) : mFile(file) { } |
| File(const File&) = delete; |
| ~File() { |
| if (mFile) { |
| ::fclose(mFile); |
| mFile = nullptr; |
| } |
| } |
| |
| FILE* get() const { return mFile; } |
| File& operator=(const File&) = delete; |
| private: |
| FILE* mFile; |
| }; |
| |
| static void printUsage(const char* program) { |
| LOGE("%s [-u user] [-g group] <namespace> <program> [options...]", program); |
| } |
| |
| static bool isNumericString(const char* str) { |
| while (isdigit(*str)) { |
| ++str; |
| } |
| return *str == '\0'; |
| } |
| |
| static std::string readNamespacePid(const char* ns) { |
| char nsPath[PATH_MAX]; |
| snprintf(nsPath, sizeof(nsPath), "%s/%s.pid", kNetNsDir, ns); |
| |
| File file(::fopen(nsPath, "r")); |
| if (file.get() == nullptr) { |
| LOGE("Unable to open file %s for namespace %s: %s", |
| nsPath, ns, strerror(errno)); |
| return std::string(); |
| } |
| |
| char buffer[32]; |
| size_t bytesRead = ::fread(buffer, 1, sizeof(buffer), file.get()); |
| if (bytesRead < sizeof(buffer) && feof(file.get())) { |
| // Reached end-of-file, null-terminate |
| buffer[bytesRead] = '\0'; |
| if (isNumericString(buffer)) { |
| // File is valid and contains a number, return it |
| return buffer; |
| } |
| LOGE("File %s does not contain a valid pid '%s'", nsPath, buffer); |
| } else if (ferror(file.get())) { |
| LOGE("Error reading from file %s: %s", nsPath, strerror(errno)); |
| } else { |
| LOGE("Invalid contents of pid file %s", nsPath); |
| } |
| return std::string(); |
| } |
| |
| static bool setNetworkNamespace(const char* ns) { |
| // There is a file in the net namespace dir (/data/vendor/var/run/netns) with |
| // the name "<namespace>.pid". This file contains the pid of the createns |
| // process that created the namespace. |
| // |
| // To switch network namespace we're going to call setns which requires an |
| // open file descriptor to /proc/<pid>/ns/net where <pid> refers to a |
| // process already running in that namespace. So using the pid from the file |
| // above we can determine which path to use. |
| std::string pid = readNamespacePid(ns); |
| if (pid.empty()) { |
| return false; |
| } |
| char nsPath[PATH_MAX]; |
| snprintf(nsPath, sizeof(nsPath), "/proc/%s/ns/net", pid.c_str()); |
| |
| FileDescriptor nsFd(open(nsPath, O_RDONLY | O_CLOEXEC)); |
| if (nsFd.get() == -1) { |
| LOGE("Cannot open network namespace '%s' at '%s': %s", |
| ns, nsPath, strerror(errno)); |
| return false; |
| } |
| |
| if (setns(nsFd.get(), CLONE_NEWNET) == -1) { |
| LOGE("Cannot set network namespace '%s': %s", |
| ns, strerror(errno)); |
| return false; |
| } |
| return true; |
| } |
| |
| static bool changeUser(const char* user) { |
| struct passwd* pwd = ::getpwnam(user); |
| if (pwd == nullptr) { |
| LOGE("Could not find user '%s'", user); |
| return false; |
| } |
| |
| if (::setuid(pwd->pw_uid) != 0) { |
| LOGE("Cannot switch to user '%s': %s", user, strerror(errno)); |
| return false; |
| } |
| return true; |
| } |
| |
| static bool changeGroup(const char* group) { |
| struct group* grp = ::getgrnam(group); |
| if (grp == nullptr) { |
| LOGE("Could not find group '%s'", group); |
| return false; |
| } |
| |
| if (::setgid(grp->gr_gid) != 0) { |
| LOGE("Cannot switch to group '%s': %s", group, strerror(errno)); |
| return false; |
| } |
| return true; |
| } |
| |
| // Append a formatted string to the end of |buffer|. The total size in |buffer| |
| // is |size|, including any existing string data. The string to append is |
| // specified by |fmt| and any additional arguments required by the format |
| // string. If the function fails it returns -1, otherwise it returns the number |
| // of characters printed (excluding the terminating NULL). On success the |
| // string is always null-terminated. |
| static int sncatf(char* buffer, size_t size, const char* fmt, ...) { |
| size_t len = strnlen(buffer, size); |
| if (len >= size) { |
| // The length exceeds the available size, if len == size then there is |
| // also a terminating null after len bytes which would then be outside |
| // the provided buffer. |
| return -1; |
| } |
| |
| va_list args; |
| va_start(args, fmt); |
| int printed = vsnprintf(buffer + len, size - len, fmt, args); |
| buffer[size - 1] = '\0'; |
| va_end(args); |
| return printed; |
| } |
| |
| /** |
| * Execute a given |command| with |argc| number of parameters that are located |
| * in |argv|. The first parameter in |argv| is the command that should be run |
| * followed by its arguments. |
| */ |
| static int execCommand( int argc, char** argv) { |
| if (argc <= 0 || argv == nullptr || argv[0] == nullptr) { |
| LOGE("No command specified"); |
| return 1; |
| } |
| |
| std::vector<char*> arguments; |
| // Place all the arguments in the vector and the terminating null |
| arguments.insert(arguments.begin(), argv, argv + argc); |
| arguments.push_back(nullptr); |
| |
| char buffer[4096]; |
| if (execvp(argv[0], arguments.data()) == -1) { |
| // Save errno in case it gets changed by printing stuff. |
| int error = errno; |
| int printed = snprintf(buffer, sizeof(buffer), |
| "Could not execute command '%s", argv[0]); |
| if (printed < 0) { |
| LOGE("Could not execute command: %s", strerror(error)); |
| return error; |
| } |
| for (int i = 1; i < argc; ++i) { |
| // Be nice to the user and print quotes if there are spaces to |
| // indicate how we saw it. If there are already single quotes in |
| // there confusion will ensue. |
| if (strchr(argv[i], ' ')) { |
| sncatf(buffer, sizeof(buffer), " \"%s\"", argv[i]); |
| } else { |
| sncatf(buffer, sizeof(buffer), " %s", argv[i]); |
| } |
| } |
| sncatf(buffer, sizeof(buffer), "': %s", strerror(error)); |
| LOGE("%s", buffer); |
| return error; |
| } |
| // execvp never returns unless it fails so this is just to return something. |
| return 0; |
| } |
| |
| /** |
| * Enter a given network namespace argv[1] and execute command argv[2] with |
| * options argv[3..argc-1] in that namespace. |
| */ |
| int main(int argc, char* argv[]) { |
| isTerminal = isatty(STDOUT_FILENO) != 0; |
| |
| // Parse parameters |
| const char* user = nullptr; |
| const char* group = nullptr; |
| int nsArg = -1; |
| int execArg = -1; |
| for (int i = 1; i < argc; ++i) { |
| if (::strcmp(argv[i], "-u") == 0) { |
| if (user || i + 1 >= argc) { |
| LOGE("Missing argument to option -u"); |
| return 1; |
| } |
| user = argv[++i]; |
| } else if (::strcmp(argv[i], "-g") == 0) { |
| if (group || i + 1 >= argc) { |
| LOGE("Missing argument to option -g"); |
| return 1; |
| } |
| group = argv[++i]; |
| } else { |
| // Break on the first non-option and treat it as the namespace name |
| nsArg = i; |
| if (i + 1 < argc) { |
| execArg = i + 1; |
| } |
| break; |
| } |
| } |
| |
| if (nsArg < 0 || execArg < 0) { |
| // Missing namespace and/or exec arguments |
| printUsage(argv[0]); |
| return 1; |
| } |
| |
| // First set the new network namespace for this process |
| if (!setNetworkNamespace(argv[nsArg])) { |
| return 1; |
| } |
| |
| // Changing namespace is the privileged operation, so now we can drop |
| // privileges by changing user and/or group if the user requested it. Note |
| // that it's important to change group first because it must be done as a |
| // privileged user. Otherwise an attacker might be able to restore group |
| // privileges by using the group ID that is saved by setgid when running |
| // as a non-privileged user. |
| if (group && !changeGroup(group)) { |
| return 1; |
| } |
| |
| if (user && !changeUser(user)) { |
| return 1; |
| } |
| |
| // Now run the command with all the remaining parameters |
| return execCommand(argc - execArg, &argv[execArg]); |
| } |
| |