update_dyld_shared_cache.mm   [plain text]


/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil -*-
 *
 * Copyright (c) 2014 Apple Inc. All rights reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <mach/mach.h>
#include <mach/mach_time.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <fcntl.h>
#include <dlfcn.h>
#include <signal.h>
#include <errno.h>
#include <sys/uio.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/sysctl.h>
#include <sys/resource.h>
#include <dirent.h>
#include <rootless.h>

extern "C" {
#include <dscsym.h>
}

#include <vector>
#include <set>
#include <map>
#include <iostream>
#include <fstream>

#include "MachOProxy.h"
#include "manifest.h"
#include "mega-dylib-utils.h"
#include "Logging.h"

#import "MultiCacheBuilder.h"

#if !__has_feature(objc_arc)
#error The use of libdispatch in this files requires it to be compiled with ARC in order to avoid leaks
#endif

const std::string anchorsDirPath = "/private/var/db/dyld/shared_region_roots";

bool parsePathsFile( const std::string& filePath, std::set<std::string>& paths ) {
    verboseLog( "parsing .paths file '%s'", filePath.c_str() );
    std::ifstream myfile( filePath );
        if ( myfile.is_open() ) {
		std::string line;
		while ( std::getline(myfile, line) ) {
			size_t pos = line.find('#');
			if ( pos != std::string::npos )
				line.resize(pos);
			while ( line.size() != 0 && isspace(line.back()) ) {
				line.pop_back();
			}
                        if ( !line.empty() ) paths.insert( line );
                }
                myfile.close();

                return true;
        }
        return false;
}

static bool parseDirectoryOfPathsFiles(const std::string& dirPath, std::set<std::string>& paths)
{
	DIR* dir = ::opendir(dirPath.c_str());
	if ( dir == NULL )
		return false;

	for (dirent* entry = ::readdir(dir); entry != NULL; entry = ::readdir(dir)) {
		if ( entry->d_type == DT_REG || entry->d_type == DT_UNKNOWN ) {
			// only look at regular files ending in .paths
			if ( strcmp(&entry->d_name[entry->d_namlen-6], ".paths") == 0 ) {
				struct stat statBuf;
				std::string filePathStr = dirPath + "/" + entry->d_name;
				const char* filePath = filePathStr.c_str();
				if ( lstat(filePath, &statBuf) == -1 ) {
					warning("can't access file '%s'", filePath);
				}
				else if ( S_ISREG(statBuf.st_mode) ) {
					parsePathsFile(filePath, paths);
				}
				else {
					warning("not a regular file '%s'", filePath);
				}
			}
			else {
				warning("ignoring file with wrong extension '%s'", entry->d_name);
			}
		}
	}
	::closedir(dir);
	return true;
}

static bool buildInitialPaths(const std::string& volumeRootPath, const std::string& overlayPath, std::set<std::string>& paths)
{
	// in -root mode, look for roots in /rootpath/private/var/db/dyld/shared_region_roots
	if ( volumeRootPath != "/" ) {
		if ( parseDirectoryOfPathsFiles(volumeRootPath + "/" + anchorsDirPath, paths) )
			return true;
		// fallback to .paths files on boot volume
		return parseDirectoryOfPathsFiles(anchorsDirPath, paths);
	}

	// in -overlay mode, look for .paths first in each /overlay/private/var/db/dyld/shared_region_roots
	if ( !overlayPath.empty() ) {
		parseDirectoryOfPathsFiles(overlayPath + "/" + anchorsDirPath, paths);
	}

	// look for .paths files in /private/var/db/dyld/shared_region_roots
	return parseDirectoryOfPathsFiles(anchorsDirPath, paths);
}

bool fileExists(const std::string& path, bool& isSymLink)
{
	struct stat statBuf;
	if ( lstat(path.c_str(), &statBuf) == -1 )
		return false;
	isSymLink = S_ISLNK(statBuf.st_mode);
	return S_ISREG(statBuf.st_mode) || isSymLink;
}

bool tryPath(const std::string& prefix, const std::string& path, std::string& foundPath, std::vector<std::string>& aliases)
{
	foundPath = prefix + path;
	bool isSymLink;
	if ( !fileExists(foundPath, isSymLink) )
		return false;
	if ( isSymLink ) {
		// handle case where install name is a symlink to real file (e.g. libstdc++.6.dylib -> libstdc++.6.0.9.dylib)
		char pathInSymLink[MAXPATHLEN];
		long len = ::readlink(foundPath.c_str(), pathInSymLink, sizeof(pathInSymLink));
		if ( len != -1 ) {
			pathInSymLink[len] = '\0';
			if ( pathInSymLink[0] != '/' ) {
				std::string aliasPath = path;
				size_t pos = aliasPath.rfind('/');
				if ( pos != std::string::npos ) {
					std::string newPath = aliasPath.substr(0,pos+1) + pathInSymLink;
					aliases.push_back(newPath);
				}
			}
		}
	}
	char realPath[MAXPATHLEN];
	if ( ::realpath(foundPath.c_str(), realPath) ) {
		if ( foundPath != realPath ) {
			std::string altPath = realPath;
			if ( !prefix.empty() ) {
				if ( altPath.substr(0, prefix.size()) == prefix ) {
					altPath = altPath.substr(prefix.size());
				}
			}
			if ( altPath != path )
				aliases.push_back(path);
			else
				aliases.push_back(altPath);
		}
	}
	return true;
}

bool improvePath(const char* volumeRootPath, const std::vector<const char*>& overlayPaths,
				 const std::string& path, std::string& foundPath, std::vector<std::string>& aliases)
{
	for (const char* overlay : overlayPaths) {
		if ( tryPath(overlay, path, foundPath, aliases) )
			return true;
	}
	if ( volumeRootPath[0] != '\0' ) {
		if ( tryPath(volumeRootPath, path, foundPath, aliases) )
			return true;
	}
	return tryPath("", path, foundPath, aliases);
}

std::string fileExists( const std::string& path ) {
    const uint8_t* p = (uint8_t*)( -1 );
    struct stat stat_buf;
	bool rootless;

    std::tie( p, stat_buf, rootless ) = fileCache.cacheLoad( path );
    if ( p != (uint8_t*)( -1 ) ) {
        return normalize_absolute_file_path( path );
    }

    return "";
}

void populateManifest(Manifest& manifest, std::set<std::string> archs, const std::string& overlayPath,
                      const std::string& rootPath, const std::set<std::string>& paths) {
	for ( const auto& arch : archs ) {
		auto fallback = fallbackArchStringForArchString(arch);
		std::set<std::string> allArchs = archs;
		std::set<std::string> processedPaths;
		std::set<std::string> unprocessedPaths = paths;
		std::set<std::string> pathsToProcess;
		std::set_difference( unprocessedPaths.begin(), unprocessedPaths.end(), processedPaths.begin(), processedPaths.end(),
							std::inserter( pathsToProcess, pathsToProcess.begin() ) );
		while ( !pathsToProcess.empty() ) {
            for (const std::string path : pathsToProcess) {
                processedPaths.insert(path);
                std::string fullPath;
                if ( rootPath != "/" ) {
                    // with -root, only look in the root path volume
                    fullPath = fileExists(rootPath + path);
                }
                else {
                    // with -overlay, look first in overlay dir
                    if ( !overlayPath.empty() )
                        fullPath = fileExists(overlayPath + path);
                    // if not in overlay, look in boot volume
                    if ( fullPath.empty() )
                        fullPath = fileExists(path);
                }
				if ( fullPath.empty() )
					continue;
				auto proxies = MachOProxy::findDylibInfo(fullPath, true, true);
				auto proxy = proxies.find(arch);
				if (proxy == proxies.end())
					proxy = proxies.find(fallback);
				if (proxy == proxies.end())
					continue;
				
				for ( const auto& dependency : proxy->second->dependencies ) {
					unprocessedPaths.insert( dependency );
				}
				
				if ( proxy->second->installName.empty() ) {
					continue;
				}
				
				proxy->second->addAlias( path );
                manifest.architectureFiles[arch].dylibs.insert(std::make_pair(proxy->second->installName,
                                                                              Manifest::File(proxy->second)));
                manifest.configurations["localhost"].architectures[arch].anchors.push_back( proxy->second->installName );
			}
			
			pathsToProcess.clear();
			std::set_difference( unprocessedPaths.begin(), unprocessedPaths.end(), processedPaths.begin(), processedPaths.end(),
								std::inserter( pathsToProcess, pathsToProcess.begin() ) );
		}
	}
}

static bool runningOnHaswell()
{
    // check system is capable of running x86_64h code
    struct host_basic_info  info;
    mach_msg_type_number_t  count    = HOST_BASIC_INFO_COUNT;
    mach_port_t             hostPort = mach_host_self();
    kern_return_t           result   = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
    mach_port_deallocate(mach_task_self(), hostPort);

    return ( (result == KERN_SUCCESS) && (info.cpu_subtype == CPU_SUBTYPE_X86_64_H) );
}


#define TERMINATE_IF_LAST_ARG( s )           \
    do {                                     \
        if ( i == argc - 1 ) terminate( s ); \
    } while ( 0 )

int main(int argc, const char* argv[])
{
    std::string rootPath;
    std::string overlayPath;
    std::string platform = "osx";
    std::string dylibListFile;
	bool universal = false;
	bool force = false;
	std::string cacheDir;
	std::set<std::string>         archStrs;
	std::vector<Manifest::Anchor> anchors;

	// parse command line options
	for (int i = 1; i < argc; ++i) {
		const char* arg = argv[i];
		if (arg[0] == '-') {
			if (strcmp(arg, "-debug") == 0) {
				setVerbose(true);
			} else if (strcmp(arg, "-verbose") == 0) {
				setVerbose(true);
			} else if (strcmp(arg, "-dont_map_local_symbols") == 0) {
				//We are going to ignore this
			} else if (strcmp(arg, "-iPhone") == 0) {
				platform = "iphoneos";
			} else if (strcmp(arg, "-dylib_list") == 0) {
				TERMINATE_IF_LAST_ARG("-dylib_list missing argument");
				dylibListFile = argv[++i];
			} else if ((strcmp(arg, "-root") == 0) || (strcmp(arg, "--root") == 0)) {
				TERMINATE_IF_LAST_ARG("-root missing path argument\n");
				rootPath = argv[++i];
			} else if (strcmp(arg, "-overlay") == 0) {
				TERMINATE_IF_LAST_ARG("-overlay missing path argument\n");
				overlayPath = argv[++i];
			} else if (strcmp(arg, "-cache_dir") == 0) {
				TERMINATE_IF_LAST_ARG("-cache_dir missing path argument\n");
				cacheDir = argv[++i];
			} else if (strcmp(arg, "-arch") == 0) {
				TERMINATE_IF_LAST_ARG("-arch missing argument\n");
				archStrs.insert(argv[++i]);
			} else if (strcmp(arg, "-force") == 0) {
				force = true;
			} else if (strcmp(arg, "-sort_by_name") == 0) {
				//No-op, we always do this now
			} else if (strcmp(arg, "-universal_boot") == 0) {
				universal = true;
			} else {
				//usage();
				terminate("unknown option: %s\n", arg);
			}
		} else {
			//usage();
			terminate("unknown option: %s\n", arg);
		}
    }

	setReturnNonZeroOnTerminate();
    setWarnAnErrorPrefixes("update_dyld_shared_cache: warning: ", "update_dyld_shared_cache failed: ");

    if ( !rootPath.empty() & !overlayPath.empty() )
        terminate("-root and -overlay cannot be used together\n");

	//FIXME realpath on root and overlays

    if (rootPath.empty()) {
        rootPath = "/";
    }

    if ( cacheDir.empty() ) {
        // write cache file into -root or -overlay directory, if used
        if ( rootPath != "/" )
            cacheDir = rootPath +  MACOSX_DYLD_SHARED_CACHE_DIR;
        else if ( !overlayPath.empty()  )
            cacheDir = overlayPath +  MACOSX_DYLD_SHARED_CACHE_DIR;
        else
            cacheDir = MACOSX_DYLD_SHARED_CACHE_DIR;
    }

    if (universal) {
        if ( platform == "iphoneos" ) {
            terminate("-iPhoneOS and -universal are incompatible\n");
        }
        archStrs.clear();
    }

    if (archStrs.size() == 0) {
		if ( platform == "iphoneos" ) {
			terminate("Must specify -arch(s) when using -iPhone\n");
		}
		else {
            if ( universal ) {
                // <rdar://problem/26182089> -universal_boot should make all possible dyld caches
                archStrs.insert("i386");
                archStrs.insert("x86_64");
                archStrs.insert("x86_64h");
            }
            else {
                // just make caches for this machine
                archStrs.insert("i386");
                archStrs.insert(runningOnHaswell() ? "x86_64h" : "x86_64");
            }
        }
    }

    int err = mkpath_np(cacheDir.c_str(), S_IRWXU | S_IRGRP|S_IXGRP | S_IROTH|S_IXOTH);
    if (err != 0 && err != EEXIST) {
        terminate("mkpath_np fail: %d", err);
    }

	Manifest manifest;

        std::set<std::string> paths;

        if ( !dylibListFile.empty() ) {
            if ( !parsePathsFile( dylibListFile, paths ) ) {
                terminate( "could not build intiial paths\n" );
            }
        } else if ( !buildInitialPaths( rootPath, overlayPath, paths ) ) {
            terminate( "could not build intiial paths\n" );
        }

        manifest.platform = platform;
        populateManifest( manifest, archStrs, overlayPath, rootPath, paths );

        // If the path we are writing to is trusted then our sources need to be trusted
        // <rdar://problem/21166835> Can't update the update_dyld_shared_cache on a non-boot volume
        bool requireDylibsBeRootlessProtected = isProtectedBySIP(cacheDir);
        manifest.calculateClosure( requireDylibsBeRootlessProtected );
        manifest.pruneClosure();

    for (const std::string& archStr : archStrs) {
        std::string cachePath = cacheDir + "/dyld_shared_cache_" + archStr;
        if ( manifest.sameContentsAsCacheAtPath("localhost", archStr, cachePath) && !force ) {
            manifest.configurations["localhost"].architectures.erase(archStr);
            verboseLog("%s is already up to date", cachePath.c_str());
        }
    }

    // If caches already up to date, do nothing
    if ( manifest.configurations["localhost"].architectures.empty() )
        dumpLogAndExit(false);

    // build caches
    std::shared_ptr<MultiCacheBuilder> builder = std::make_shared<MultiCacheBuilder>(manifest, false, false, false, false, requireDylibsBeRootlessProtected);
    builder->buildCaches(cacheDir);

    // Save off spintrace data
    std::string nuggetRoot = (overlayPath.empty() ? rootPath : overlayPath);
    (void)dscsym_save_nuggets_for_current_caches(nuggetRoot.c_str());

	// Now that all the build commands have been issued lets put a barrier in after then which can tear down the app after
	// everything is written.
	builder->logStats();
	dumpLogAndExit(false);

	dispatch_main();
}