blob: 770d24992e5a0a20a52172f9d5a35528e33a3bda [file] [log] [blame]
/*
* 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]);
}