diff --git a/run-as/Android.mk b/run-as/Android.mk index 3774acc8f..2e305d7fc 100644 --- a/run-as/Android.mk +++ b/run-as/Android.mk @@ -1,12 +1,8 @@ LOCAL_PATH:= $(call my-dir) + include $(CLEAR_VARS) - -LOCAL_SRC_FILES := run-as.c package.c - -LOCAL_SHARED_LIBRARIES := libselinux - -LOCAL_MODULE := run-as - LOCAL_CFLAGS := -Werror - +LOCAL_MODULE := run-as +LOCAL_SHARED_LIBRARIES := libselinux libpackagelistparser +LOCAL_SRC_FILES := run-as.cpp include $(BUILD_EXECUTABLE) diff --git a/run-as/package.c b/run-as/package.c deleted file mode 100644 index aea89e56b..000000000 --- a/run-as/package.c +++ /dev/null @@ -1,556 +0,0 @@ -/* -** -** Copyright 2010, 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. -*/ -#include -#include -#include -#include -#include -#include -#include - -#include -#include "package.h" - -/* - * WARNING WARNING WARNING WARNING - * - * The following code runs as root on production devices, before - * the run-as command has dropped the uid/gid. Hence be very - * conservative and keep in mind the following: - * - * - Performance does not matter here, clarity and safety of the code - * does however. Documentation is a must. - * - * - Avoid calling C library functions with complex implementations - * like malloc() and printf(). You want to depend on simple system - * calls instead, which behaviour is not going to be altered in - * unpredictible ways by environment variables or system properties. - * - * - Do not trust user input and/or the filesystem whenever possible. - * - */ - -/* The file containing the list of installed packages on the system */ -#define PACKAGES_LIST_FILE "/data/system/packages.list" - -/* Copy 'srclen' string bytes from 'src' into buffer 'dst' of size 'dstlen' - * This function always zero-terminate the destination buffer unless - * 'dstlen' is 0, even in case of overflow. - * Returns a pointer into the src string, leaving off where the copy - * has stopped. The copy will stop when dstlen, srclen or a null - * character on src has been reached. - */ -static const char* -string_copy(char* dst, size_t dstlen, const char* src, size_t srclen) -{ - const char* srcend = src + srclen; - const char* dstend = dst + dstlen; - - if (dstlen == 0) - return src; - - dstend--; /* make room for terminating zero */ - - while (dst < dstend && src < srcend && *src != '\0') - *dst++ = *src++; - - *dst = '\0'; /* zero-terminate result */ - return src; -} - -/* Open 'filename' and map it into our address-space. - * Returns buffer address, or NULL on error - * On exit, *filesize will be set to the file's size, or 0 on error - */ -static void* -map_file(const char* filename, size_t* filesize) -{ - int fd, ret, old_errno; - struct stat st; - size_t length = 0; - void* address = NULL; - gid_t oldegid; - - *filesize = 0; - - /* - * Temporarily switch effective GID to allow us to read - * the packages file - */ - - oldegid = getegid(); - if (setegid(AID_PACKAGE_INFO) < 0) { - return NULL; - } - - /* open the file for reading */ - fd = TEMP_FAILURE_RETRY(open(filename, O_RDONLY)); - if (fd < 0) { - return NULL; - } - - /* restore back to our old egid */ - if (setegid(oldegid) < 0) { - goto EXIT; - } - - /* get its size */ - ret = TEMP_FAILURE_RETRY(fstat(fd, &st)); - if (ret < 0) - goto EXIT; - - /* Ensure that the file is owned by the system user */ - if ((st.st_uid != AID_SYSTEM) || (st.st_gid != AID_PACKAGE_INFO)) { - goto EXIT; - } - - /* Ensure that the file has sane permissions */ - if ((st.st_mode & S_IWOTH) != 0) { - goto EXIT; - } - - /* Ensure that the size is not ridiculously large */ - length = (size_t)st.st_size; - if ((off_t)length != st.st_size) { - errno = ENOMEM; - goto EXIT; - } - - /* Memory-map the file now */ - do { - address = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0); - } while (address == MAP_FAILED && errno == EINTR); - if (address == MAP_FAILED) { - address = NULL; - goto EXIT; - } - - /* We're good, return size */ - *filesize = length; - -EXIT: - /* close the file, preserve old errno for better diagnostics */ - old_errno = errno; - close(fd); - errno = old_errno; - - return address; -} - -/* unmap the file, but preserve errno */ -static void -unmap_file(void* address, size_t size) -{ - int old_errno = errno; - TEMP_FAILURE_RETRY(munmap(address, size)); - errno = old_errno; -} - -/* Check that a given directory: - * - exists - * - is owned by a given uid/gid - * - is a real directory, not a symlink - * - isn't readable or writable by others - * - * Return 0 on success, or -1 on error. - * errno is set to EINVAL in case of failed check. - */ -static int -check_directory_ownership(const char* path, uid_t uid) -{ - int ret; - struct stat st; - - do { - ret = lstat(path, &st); - } while (ret < 0 && errno == EINTR); - - if (ret < 0) - return -1; - - /* must be a real directory, not a symlink */ - if (!S_ISDIR(st.st_mode)) - goto BAD; - - /* must be owned by specific uid/gid */ - if (st.st_uid != uid || st.st_gid != uid) - goto BAD; - - /* must not be readable or writable by others */ - if ((st.st_mode & (S_IROTH|S_IWOTH)) != 0) - goto BAD; - - /* everything ok */ - return 0; - -BAD: - errno = EINVAL; - return -1; -} - -/* This function is used to check the data directory path for safety. - * We check that every sub-directory is owned by the 'system' user - * and exists and is not a symlink. We also check that the full directory - * path is properly owned by the user ID. - * - * Return 0 on success, -1 on error. - */ -int -check_data_path(const char* dataPath, uid_t uid) -{ - int nn; - - /* the path should be absolute */ - if (dataPath[0] != '/') { - errno = EINVAL; - return -1; - } - - /* look for all sub-paths, we do that by finding - * directory separators in the input path and - * checking each sub-path independently - */ - for (nn = 1; dataPath[nn] != '\0'; nn++) - { - char subpath[PATH_MAX]; - - /* skip non-separator characters */ - if (dataPath[nn] != '/') - continue; - - /* handle trailing separator case */ - if (dataPath[nn+1] == '\0') { - break; - } - - /* found a separator, check that dataPath is not too long. */ - if (nn >= (int)(sizeof subpath)) { - errno = EINVAL; - return -1; - } - - /* reject any '..' subpath */ - if (nn >= 3 && - dataPath[nn-3] == '/' && - dataPath[nn-2] == '.' && - dataPath[nn-1] == '.') { - errno = EINVAL; - return -1; - } - - /* copy to 'subpath', then check ownership */ - memcpy(subpath, dataPath, nn); - subpath[nn] = '\0'; - - if (check_directory_ownership(subpath, AID_SYSTEM) < 0) - return -1; - } - - /* All sub-paths were checked, now verify that the full data - * directory is owned by the application uid - */ - if (check_directory_ownership(dataPath, uid) < 0) - return -1; - - /* all clear */ - return 0; -} - -/* Return TRUE iff a character is a space or tab */ -static inline int -is_space(char c) -{ - return (c == ' ' || c == '\t'); -} - -/* Skip any space or tab character from 'p' until 'end' is reached. - * Return new position. - */ -static const char* -skip_spaces(const char* p, const char* end) -{ - while (p < end && is_space(*p)) - p++; - - return p; -} - -/* Skip any non-space and non-tab character from 'p' until 'end'. - * Return new position. - */ -static const char* -skip_non_spaces(const char* p, const char* end) -{ - while (p < end && !is_space(*p)) - p++; - - return p; -} - -/* Find the first occurence of 'ch' between 'p' and 'end' - * Return its position, or 'end' if none is found. - */ -static const char* -find_first(const char* p, const char* end, char ch) -{ - while (p < end && *p != ch) - p++; - - return p; -} - -/* Check that the non-space string starting at 'p' and eventually - * ending at 'end' equals 'name'. Return new position (after name) - * on success, or NULL on failure. - * - * This function fails is 'name' is NULL, empty or contains any space. - */ -static const char* -compare_name(const char* p, const char* end, const char* name) -{ - /* 'name' must not be NULL or empty */ - if (name == NULL || name[0] == '\0' || p == end) - return NULL; - - /* compare characters to those in 'name', excluding spaces */ - while (*name) { - /* note, we don't check for *p == '\0' since - * it will be caught in the next conditional. - */ - if (p >= end || is_space(*p)) - goto BAD; - - if (*p != *name) - goto BAD; - - p++; - name++; - } - - /* must be followed by end of line or space */ - if (p < end && !is_space(*p)) - goto BAD; - - return p; - -BAD: - return NULL; -} - -/* Parse one or more whitespace characters starting from '*pp' - * until 'end' is reached. Updates '*pp' on exit. - * - * Return 0 on success, -1 on failure. - */ -static int -parse_spaces(const char** pp, const char* end) -{ - const char* p = *pp; - - if (p >= end || !is_space(*p)) { - errno = EINVAL; - return -1; - } - p = skip_spaces(p, end); - *pp = p; - return 0; -} - -/* Parse a positive decimal number starting from '*pp' until 'end' - * is reached. Adjust '*pp' on exit. Return decimal value or -1 - * in case of error. - * - * If the value is larger than INT_MAX, -1 will be returned, - * and errno set to EOVERFLOW. - * - * If '*pp' does not start with a decimal digit, -1 is returned - * and errno set to EINVAL. - */ -static int -parse_positive_decimal(const char** pp, const char* end) -{ - const char* p = *pp; - int value = 0; - int overflow = 0; - - if (p >= end || *p < '0' || *p > '9') { - errno = EINVAL; - return -1; - } - - while (p < end) { - int ch = *p; - unsigned d = (unsigned)(ch - '0'); - int val2; - - if (d >= 10U) /* d is unsigned, no lower bound check */ - break; - - val2 = value*10 + (int)d; - if (val2 < value) - overflow = 1; - value = val2; - p++; - } - *pp = p; - - if (overflow) { - errno = EOVERFLOW; - value = -1; - } - return value; -} - -/* Read the system's package database and extract information about - * 'pkgname'. Return 0 in case of success, or -1 in case of error. - * - * If the package is unknown, return -1 and set errno to ENOENT - * If the package database is corrupted, return -1 and set errno to EINVAL - */ -int -get_package_info(const char* pkgName, uid_t userId, PackageInfo *info) -{ - char* buffer; - size_t buffer_len; - const char* p; - const char* buffer_end; - int result = -1; - - info->uid = 0; - info->isDebuggable = 0; - info->dataDir[0] = '\0'; - info->seinfo[0] = '\0'; - - buffer = map_file(PACKAGES_LIST_FILE, &buffer_len); - if (buffer == NULL) - return -1; - - p = buffer; - buffer_end = buffer + buffer_len; - - /* expect the following format on each line of the control file: - * - * - * - * where: - * is the package's name - * is the application-specific user Id (decimal) - * is 1 if the package is debuggable, or 0 otherwise - * is the path to the package's data directory (e.g. /data/data/com.example.foo) - * is the seinfo label associated with the package - * - * The file is generated in com.android.server.PackageManagerService.Settings.writeLP() - */ - - while (p < buffer_end) { - /* find end of current line and start of next one */ - const char* end = find_first(p, buffer_end, '\n'); - const char* next = (end < buffer_end) ? end + 1 : buffer_end; - const char* q; - int uid, debugFlag; - - /* first field is the package name */ - p = compare_name(p, end, pkgName); - if (p == NULL) - goto NEXT_LINE; - - /* skip spaces */ - if (parse_spaces(&p, end) < 0) - goto BAD_FORMAT; - - /* second field is the pid */ - uid = parse_positive_decimal(&p, end); - if (uid < 0) - return -1; - - info->uid = (uid_t) uid; - - /* skip spaces */ - if (parse_spaces(&p, end) < 0) - goto BAD_FORMAT; - - /* third field is debug flag (0 or 1) */ - debugFlag = parse_positive_decimal(&p, end); - switch (debugFlag) { - case 0: - info->isDebuggable = 0; - break; - case 1: - info->isDebuggable = 1; - break; - default: - goto BAD_FORMAT; - } - - /* skip spaces */ - if (parse_spaces(&p, end) < 0) - goto BAD_FORMAT; - - /* fourth field is data directory path and must not contain - * spaces. - */ - q = skip_non_spaces(p, end); - if (q == p) - goto BAD_FORMAT; - - /* If userId == 0 (i.e. user is device owner) we can use dataDir value - * from packages.list, otherwise compose data directory as - * /data/user/$uid/$packageId - */ - if (userId == 0) { - p = string_copy(info->dataDir, sizeof info->dataDir, p, q - p); - } else { - snprintf(info->dataDir, - sizeof info->dataDir, - "/data/user/%d/%s", - userId, - pkgName); - p = q; - } - - /* skip spaces */ - if (parse_spaces(&p, end) < 0) - goto BAD_FORMAT; - - /* fifth field is the seinfo string */ - q = skip_non_spaces(p, end); - if (q == p) - goto BAD_FORMAT; - - string_copy(info->seinfo, sizeof info->seinfo, p, q - p); - - /* Ignore the rest */ - result = 0; - goto EXIT; - - NEXT_LINE: - p = next; - } - - /* the package is unknown */ - errno = ENOENT; - result = -1; - goto EXIT; - -BAD_FORMAT: - errno = EINVAL; - result = -1; - -EXIT: - unmap_file(buffer, buffer_len); - return result; -} diff --git a/run-as/package.h b/run-as/package.h deleted file mode 100644 index eeb59137d..000000000 --- a/run-as/package.h +++ /dev/null @@ -1,44 +0,0 @@ -/* -** -** Copyright 2010, 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. -*/ -#ifndef RUN_AS_PACKAGE_H -#define RUN_AS_PACKAGE_H - -#include -#include - -typedef enum { - PACKAGE_IS_DEBUGGABLE = 0, - PACKAGE_IS_NOT_DEBUGGABLE, - PACKAGE_IS_UNKNOWN, -} PackageStatus; - -typedef struct { - uid_t uid; - char isDebuggable; - char dataDir[PATH_MAX]; - char seinfo[PATH_MAX]; -} PackageInfo; - -/* see documentation in package.c for these functions */ - -extern int get_package_info(const char* packageName, - uid_t userId, - PackageInfo* info); - -extern int check_data_path(const char* dataDir, uid_t uid); - -#endif /* RUN_AS_PACKAGE_H */ diff --git a/run-as/run-as.c b/run-as/run-as.c deleted file mode 100644 index f0fd2fe7c..000000000 --- a/run-as/run-as.c +++ /dev/null @@ -1,224 +0,0 @@ -/* -** -** Copyright 2010, 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 PROGNAME "run-as" -#define LOG_TAG PROGNAME - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "package.h" - -/* - * WARNING WARNING WARNING WARNING - * - * This program runs with CAP_SETUID and CAP_SETGID capabilities on Android - * production devices. Be very conservative when modifying it to avoid any - * serious security issue. Keep in mind the following: - * - * - This program should only run for the 'root' or 'shell' users - * - * - Avoid anything that is more complex than simple system calls - * until the uid/gid has been dropped to that of a normal user - * or you are sure to exit. - * - * This avoids depending on environment variables, system properties - * and other external factors that may affect the C library in - * unpredictable ways. - * - * - Do not trust user input and/or the filesystem whenever possible. - * - * Read README.TXT for more details. - * - * - * - * The purpose of this program is to run a command as a specific - * application user-id. Typical usage is: - * - * run-as - * - * The 'run-as' binary is installed with CAP_SETUID and CAP_SETGID file - * capabilities, but will check the following: - * - * - that it is invoked from the 'shell' or 'root' user (abort otherwise) - * - that '' is the name of an installed and debuggable package - * - that the package's data directory is well-formed (see package.c) - * - * If so, it will drop to the application's user id / group id, cd to the - * package's data directory, then run the command there. - * - * NOTE: In the future it might not be possible to cd to the package's data - * directory under that package's user id / group id, in which case this - * utility will need to be changed accordingly. - * - * This can be useful for a number of different things on production devices: - * - * - Allow application developers to look at their own applicative data - * during development. - * - * - Run the 'gdbserver' binary executable to allow native debugging - */ - -__noreturn static void -panic(const char* format, ...) -{ - va_list args; - int e = errno; - - fprintf(stderr, "%s: ", PROGNAME); - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); - exit(e ? -e : 1); -} - -static void -usage(void) -{ - panic("Usage:\n " PROGNAME " [--user ] []\n"); -} - -int main(int argc, char **argv) -{ - const char* pkgname; - uid_t myuid, uid, gid, userAppId = 0; - int commandArgvOfs = 2, userId = 0; - PackageInfo info; - struct __user_cap_header_struct capheader; - struct __user_cap_data_struct capdata[2]; - - /* check arguments */ - if (argc < 2) { - usage(); - } - - /* check userid of caller - must be 'shell' or 'root' */ - myuid = getuid(); - if (myuid != AID_SHELL && myuid != AID_ROOT) { - panic("only 'shell' or 'root' users can run this program\n"); - } - - memset(&capheader, 0, sizeof(capheader)); - memset(&capdata, 0, sizeof(capdata)); - capheader.version = _LINUX_CAPABILITY_VERSION_3; - capdata[CAP_TO_INDEX(CAP_SETUID)].effective |= CAP_TO_MASK(CAP_SETUID); - capdata[CAP_TO_INDEX(CAP_SETGID)].effective |= CAP_TO_MASK(CAP_SETGID); - capdata[CAP_TO_INDEX(CAP_SETUID)].permitted |= CAP_TO_MASK(CAP_SETUID); - capdata[CAP_TO_INDEX(CAP_SETGID)].permitted |= CAP_TO_MASK(CAP_SETGID); - - if (capset(&capheader, &capdata[0]) < 0) { - panic("Could not set capabilities: %s\n", strerror(errno)); - } - - pkgname = argv[1]; - - /* get user_id from command line if provided */ - if ((argc >= 4) && !strcmp(argv[2], "--user")) { - userId = atoi(argv[3]); - if (userId < 0) - panic("Negative user id %d is provided\n", userId); - commandArgvOfs += 2; - } - - /* retrieve package information from system (does setegid) */ - if (get_package_info(pkgname, userId, &info) < 0) { - panic("Package '%s' is unknown\n", pkgname); - } - - /* verify that user id is not too big. */ - if ((UID_MAX - info.uid) / AID_USER < (uid_t)userId) { - panic("User id %d is too big\n", userId); - } - - /* calculate user app ID. */ - userAppId = (AID_USER * userId) + info.uid; - - /* reject system packages */ - if (userAppId < AID_APP) { - panic("Package '%s' is not an application\n", pkgname); - } - - /* reject any non-debuggable package */ - if (!info.isDebuggable) { - panic("Package '%s' is not debuggable\n", pkgname); - } - - /* check that the data directory path is valid */ - if (check_data_path(info.dataDir, userAppId) < 0) { - panic("Package '%s' has corrupt installation\n", pkgname); - } - - /* Ensure that we change all real/effective/saved IDs at the - * same time to avoid nasty surprises. - */ - uid = gid = userAppId; - if(setresgid(gid,gid,gid) || setresuid(uid,uid,uid)) { - panic("Permission denied\n"); - } - - /* Required if caller has uid and gid all non-zero */ - memset(&capdata, 0, sizeof(capdata)); - if (capset(&capheader, &capdata[0]) < 0) { - panic("Could not clear all capabilities: %s\n", strerror(errno)); - } - - if (selinux_android_setcontext(uid, 0, info.seinfo, pkgname) < 0) { - panic("Could not set SELinux security context: %s\n", strerror(errno)); - } - - // cd into the data directory, and set $HOME correspondingly. - if (TEMP_FAILURE_RETRY(chdir(info.dataDir)) < 0) { - panic("Could not cd to package's data directory: %s\n", strerror(errno)); - } - setenv("HOME", info.dataDir, 1); - - // Reset parts of the environment, like su would. - setenv("PATH", _PATH_DEFPATH, 1); - unsetenv("IFS"); - - // Set the user-specific parts for this user. - struct passwd* pw = getpwuid(uid); - setenv("LOGNAME", pw->pw_name, 1); - setenv("SHELL", pw->pw_shell, 1); - setenv("USER", pw->pw_name, 1); - - /* User specified command for exec. */ - if ((argc >= commandArgvOfs + 1) && - (execvp(argv[commandArgvOfs], argv+commandArgvOfs) < 0)) { - panic("exec failed for %s: %s\n", argv[commandArgvOfs], strerror(errno)); - } - - /* Default exec shell. */ - execlp("/system/bin/sh", "sh", NULL); - - panic("exec failed: %s\n", strerror(errno)); -} diff --git a/run-as/run-as.cpp b/run-as/run-as.cpp new file mode 100644 index 000000000..50b47b99c --- /dev/null +++ b/run-as/run-as.cpp @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2010 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// The purpose of this program is to run a command as a specific +// application user-id. Typical usage is: +// +// run-as +// +// The 'run-as' binary is installed with CAP_SETUID and CAP_SETGID file +// capabilities, but will check the following: +// +// - that it is invoked from the 'shell' or 'root' user (abort otherwise) +// - that '' is the name of an installed and debuggable package +// - that the package's data directory is well-formed +// +// If so, it will drop to the application's user id / group id, cd to the +// package's data directory, then run the command there. +// +// This can be useful for a number of different things on production devices: +// +// - Allow application developers to look at their own application data +// during development. +// +// - Run the 'gdbserver' binary executable to allow native debugging +// + +static bool packagelist_parse_callback(pkg_info* this_package, void* userdata) { + pkg_info* p = reinterpret_cast(userdata); + if (strcmp(p->name, this_package->name) == 0) { + *p = *this_package; + return false; // Stop searching. + } + packagelist_free(this_package); + return true; // Keep searching. +} + +static bool check_directory(const char* path, uid_t uid) { + struct stat st; + if (TEMP_FAILURE_RETRY(lstat(path, &st)) == -1) return false; + + // /data/user/0 is a known safe symlink. + if (strcmp("/data/user/0", path) == 0) return true; + + // Must be a real directory, not a symlink. + if (!S_ISDIR(st.st_mode)) return false; + + // Must be owned by specific uid/gid. + if (st.st_uid != uid || st.st_gid != uid) return false; + + // Must not be readable or writable by others. + if ((st.st_mode & (S_IROTH|S_IWOTH)) != 0) return false; + + return true; +} + +// This function is used to check the data directory path for safety. +// We check that every sub-directory is owned by the 'system' user +// and exists and is not a symlink. We also check that the full directory +// path is properly owned by the user ID. +static bool check_data_path(const char* data_path, uid_t uid) { + // The path should be absolute. + if (data_path[0] != '/') return false; + + // Look for all sub-paths, we do that by finding + // directory separators in the input path and + // checking each sub-path independently. + for (int nn = 1; data_path[nn] != '\0'; nn++) { + char subpath[PATH_MAX]; + + /* skip non-separator characters */ + if (data_path[nn] != '/') continue; + + /* handle trailing separator case */ + if (data_path[nn+1] == '\0') break; + + /* found a separator, check that data_path is not too long. */ + if (nn >= (int)(sizeof subpath)) return false; + + /* reject any '..' subpath */ + if (nn >= 3 && + data_path[nn-3] == '/' && + data_path[nn-2] == '.' && + data_path[nn-1] == '.') { + return false; + } + + /* copy to 'subpath', then check ownership */ + memcpy(subpath, data_path, nn); + subpath[nn] = '\0'; + + if (!check_directory(subpath, AID_SYSTEM)) return false; + } + + // All sub-paths were checked, now verify that the full data + // directory is owned by the application uid. + return check_directory(data_path, uid); +} + +int main(int argc, char* argv[]) { + // Check arguments. + if (argc < 2) { + error(1, 0, "usage: run-as [--user ] []\n"); + } + + // This program runs with CAP_SETUID and CAP_SETGID capabilities on Android + // production devices. Check user id of caller --- must be 'shell' or 'root'. + if (getuid() != AID_SHELL && getuid() != AID_ROOT) { + error(1, 0, "only 'shell' or 'root' users can run this program"); + } + + __user_cap_header_struct capheader; + __user_cap_data_struct capdata[2]; + memset(&capheader, 0, sizeof(capheader)); + memset(&capdata, 0, sizeof(capdata)); + capheader.version = _LINUX_CAPABILITY_VERSION_3; + capdata[CAP_TO_INDEX(CAP_SETUID)].effective |= CAP_TO_MASK(CAP_SETUID); + capdata[CAP_TO_INDEX(CAP_SETGID)].effective |= CAP_TO_MASK(CAP_SETGID); + capdata[CAP_TO_INDEX(CAP_SETUID)].permitted |= CAP_TO_MASK(CAP_SETUID); + capdata[CAP_TO_INDEX(CAP_SETGID)].permitted |= CAP_TO_MASK(CAP_SETGID); + if (capset(&capheader, &capdata[0]) == -1) { + error(1, errno, "couldn't set capabilities"); + } + + char* pkgname = argv[1]; + int cmd_argv_offset = 2; + + // Get user_id from command line if provided. + int userId = 0; + if ((argc >= 4) && !strcmp(argv[2], "--user")) { + userId = atoi(argv[3]); + if (userId < 0) error(1, 0, "negative user id: %d", userId); + cmd_argv_offset += 2; + } + + // Retrieve package information from system, switching egid so we can read the file. + gid_t old_egid = getegid(); + if (setegid(AID_PACKAGE_INFO) == -1) error(1, errno, "setegid(AID_PACKAGE_INFO) failed"); + pkg_info info; + memset(&info, 0, sizeof(info)); + info.name = pkgname; + if (!packagelist_parse(packagelist_parse_callback, &info)) { + error(1, errno, "packagelist_parse failed"); + } + if (info.uid == 0) { + error(1, 0, "unknown package: %s", pkgname); + } + if (setegid(old_egid) == -1) error(1, errno, "couldn't restore egid"); + + // Verify that user id is not too big. + if ((UID_MAX - info.uid) / AID_USER < (uid_t)userId) { + error(1, 0, "user id too big: %d", userId); + } + + // Calculate user app ID. + uid_t userAppId = (AID_USER * userId) + info.uid; + + // Reject system packages. + if (userAppId < AID_APP) { + error(1, 0, "package not an application: %s", pkgname); + } + + // Reject any non-debuggable package. + if (!info.debuggable) { + error(1, 0, "package not debuggable: %s", pkgname); + } + + // Check that the data directory path is valid. + if (!check_data_path(info.data_dir, userAppId)) { + error(1, 0, "package has corrupt installation: %s", pkgname); + } + + // Ensure that we change all real/effective/saved IDs at the + // same time to avoid nasty surprises. + uid_t uid = userAppId; + uid_t gid = userAppId; + if (setresgid(gid, gid, gid) == -1) { + error(1, errno, "setresgid failed"); + } + if (setresuid(uid, uid, uid) == -1) { + error(1, errno, "setresuid failed"); + } + + // Required if caller has uid and gid all non-zero. + memset(&capdata, 0, sizeof(capdata)); + if (capset(&capheader, &capdata[0]) == -1) { + error(1, errno, "couldn't clear all capabilities"); + } + + if (selinux_android_setcontext(uid, 0, info.seinfo, pkgname) < 0) { + error(1, errno, "couldn't set SELinux security context"); + } + + // cd into the data directory, and set $HOME correspondingly. + if (TEMP_FAILURE_RETRY(chdir(info.data_dir)) == -1) { + error(1, errno, "couldn't chdir to package's data directory"); + } + setenv("HOME", info.data_dir, 1); + + // Reset parts of the environment, like su would. + setenv("PATH", _PATH_DEFPATH, 1); + unsetenv("IFS"); + + // Set the user-specific parts for this user. + passwd* pw = getpwuid(uid); + setenv("LOGNAME", pw->pw_name, 1); + setenv("SHELL", pw->pw_shell, 1); + setenv("USER", pw->pw_name, 1); + + // User specified command for exec. + if ((argc >= cmd_argv_offset + 1) && + (execvp(argv[cmd_argv_offset], argv+cmd_argv_offset) == -1)) { + error(1, errno, "exec failed for %s", argv[cmd_argv_offset]); + } + + // Default exec shell. + execlp(_PATH_BSHELL, "sh", NULL); + error(1, errno, "exec failed"); +}