/*
* Copyright (c) 2020 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 "system-version-compat-support.h"
#if SYSTEM_VERSION_COMPAT_ENABLED
#include <fcntl.h>
#include <stdbool.h>
#include <strings.h>
#include <sys/errno.h>
#include <sys/param.h>
#include <unistd.h>
#define PLAT_PREFIX_IOS "iOS"
#define PLAT_PREFIX_MACOS ""
#define COMPAT_SUFFIX_MACOS "Compat"
#define COMPAT_SUFFIX_IOS ""
#define SYSTEM_VERSION_PLIST_FILENAME "SystemVersion.plist"
#define SYSTEM_VERSION_PLIST_PATH ("/System/Library/CoreServices/" SYSTEM_VERSION_PLIST_FILENAME)
#define SYSTEM_VERSION_COMPAT_PLIST_FILENAME(platform_prefix, compat_suffix) (platform_prefix "SystemVersion" compat_suffix ".plist")
#define SYSTEM_VERSION_PLIST_FILENAMELEN strlen(SYSTEM_VERSION_PLIST_FILENAME)
#define SYSTEM_VERSION_COMPAT_PLIST_FILENAMELEN(platform_prefix, compat_suffix) strlen(SYSTEM_VERSION_COMPAT_PLIST_FILENAME(platform_prefix, compat_suffix))
#define SYSTEM_VERSION_PLIST_PATHLEN strlen(SYSTEM_VERSION_PLIST_PATH)
extern system_version_compat_mode_t system_version_compat_mode;
/*
* This routine determines whether the path specified matches the path of the SystemVersion plist file
* we are shimming accesses to. If the file name suffix matches, it's expected we'll call into the
* version_compat_open_shim() routine below which will do a full comparison on the expanded path.
*
* Parameters: orig_path The path suffix that was provided to the open{at} call.
*
* Returns: true if the path suffix matches the SystemVersion plist path we're shimming
* false otherwise
*/
__attribute__((visibility("hidden")))
bool
_system_version_compat_check_path_suffix(const char *orig_path)
{
size_t path_str_len = strnlen(orig_path, MAXPATHLEN);
/*
* If the length of the filename we're opening is shorter than
* SYSTEM_VERSION_PLIST_FILENAME, bail.
*/
if (path_str_len < SYSTEM_VERSION_PLIST_FILENAMELEN) {
return false;
}
/* If the path we're accessing doesn't end in SYSTEM_VERSION_PLIST_FILENAME, bail. */
if (strncmp(&orig_path[path_str_len - SYSTEM_VERSION_PLIST_FILENAMELEN], SYSTEM_VERSION_PLIST_FILENAME,
SYSTEM_VERSION_PLIST_FILENAMELEN) != 0) {
return false;
}
/* If modifying the path specified would exceed MAXPATHLEN, bail */
if (path_str_len == MAXPATHLEN) {
return false;
}
size_t compat_len = (system_version_compat_mode == SYSTEM_VERSION_COMPAT_MODE_IOS) ? SYSTEM_VERSION_COMPAT_PLIST_FILENAMELEN(PLAT_PREFIX_IOS, COMPAT_SUFFIX_IOS) : SYSTEM_VERSION_COMPAT_PLIST_FILENAMELEN(PLAT_PREFIX_MACOS, COMPAT_SUFFIX_MACOS);
if ((compat_len - SYSTEM_VERSION_PLIST_FILENAMELEN) > (MAXPATHLEN - path_str_len - 1)) {
return false;
}
/* Indicate that we should */
return true;
}
/*
* This routine determines whether we are trying to open the SystemVersion plist at SYSTEM_VERSION_PLIST_PATH.
* It's only used on targets that have the compatibility shim enabled (mainly older binaries).
*
* Note that this routine should * ABSOLUTELY NOT * be used as a general shim for accesses at all paths. We replace
* what the developer generally expected to be one system call with multiple additional system calls. We're ok
* with doing this here because we only do it for calls to open against files that match this very specific pattern
* (named SystemVersion.plist), but doing so for all calls to open could result in various issues. Specifically it's
* difficult to ensure the same cancellation semantics (around EINTR etc) that developers generally expect when replacing
* a single system call with multiple.
*
* This routine should return with the same semantics as the general open system calls that it is shimming - specifically
* it should leave errno and the return value matching what developers expect.
*
* It's expected that _version_compat_check_path_suffix() above was called prior to this call and returned true.
*
* We take the close, open and fcntl syscalls as parameters to make sure the variant we call matches the original call
* to open{at}.
*
* Parameters: opened_fd The file descriptor that was opened in the original open{at} call
* openat_fd The file descriptor passed to the original openat call (only used when use_openat is true)
* orig_path The original path suffix passed to open{at}
* oflag The original oflag passed to open{at}
* mode The original mode passed to open{at}
* close_syscall The appropriate syscall to use for closing file descriptors
* open_syscall The syscall that should be used for a new call to open.
* fctnl_syscall The appopriate syscall to use for fcntl.
*
* Returns: The original file descriptor if the open{at} access wasn't to SYSTEM_VERSION_PLIST_PATH
* A new file descriptor (with the original closed) if the expanded path matches SYSTEM_VERSION_PLIST_PATH
* The original file descriptor if the full path suffix does not match SYSTEM_VERSION_PLIST_PATH
* -1 (with errno set to EINTR) if the new open or fcntl calls received EINTR (with all new fds closed)
*/
__attribute__((visibility("hidden")))
int
_system_version_compat_open_shim(int opened_fd, int openat_fd, const char *orig_path, int oflag, mode_t mode,
int (*close_syscall)(int), int (*open_syscall)(const char *, int, mode_t),
int (*openat_syscall)(int, const char *, int, mode_t),
int (*fcntl_syscall)(int, int, long))
{
/* stash the errno from the original open{at} call */
int stashed_errno = errno;
char new_path[MAXPATHLEN];
size_t path_str_len = strnlen(orig_path, sizeof(new_path));
/* Resolve the full path of the file we've opened */
if (fcntl_syscall(opened_fd, F_GETPATH, new_path)) {
if (errno == EINTR) {
/* If we got EINTR, we close the file that was opened and return -1 & EINTR */
close_syscall(opened_fd);
errno = EINTR;
return -1;
} else {
/* otherwise we return the original file descriptor that was requested */
errno = stashed_errno;
return opened_fd;
}
}
/* Check to see whether the path matches SYSTEM_VERSION_PLIST_PATH */
size_t newpathlen = strnlen(new_path, MAXPATHLEN);
if (newpathlen != SYSTEM_VERSION_PLIST_PATHLEN) {
errno = stashed_errno;
return opened_fd;
}
if (strncmp(new_path, SYSTEM_VERSION_PLIST_PATH, SYSTEM_VERSION_PLIST_PATHLEN) != 0) {
errno = stashed_errno;
return opened_fd;
}
new_path[0] = '\0';
/*
* It looks like we're trying to access the SystemVersion plist. Let's try to open
* the compatibility plist and return that instead if it exists.
*/
size_t prefix_str_len = path_str_len - SYSTEM_VERSION_PLIST_FILENAMELEN;
strlcpy(new_path, orig_path, (prefix_str_len + 1));
if (system_version_compat_mode == SYSTEM_VERSION_COMPAT_MODE_IOS) {
strlcat(new_path, SYSTEM_VERSION_COMPAT_PLIST_FILENAME(PLAT_PREFIX_IOS, COMPAT_SUFFIX_IOS), MAXPATHLEN);
} else {
strlcat(new_path, SYSTEM_VERSION_COMPAT_PLIST_FILENAME(PLAT_PREFIX_MACOS, COMPAT_SUFFIX_MACOS), MAXPATHLEN);
}
int new_fd = -1;
if (openat_syscall != NULL) {
new_fd = openat_syscall(openat_fd, new_path, oflag, mode);
} else {
new_fd = open_syscall(new_path, oflag, mode);
}
if ((new_fd == -1) && (errno == ENOENT)) {
/* The file doesn't exist, so return the original fd and errno. */
errno = stashed_errno;
return opened_fd;
}
/*
* Otherwise we close the first file we opened and populate errno
* with errno from the call to open{at}. (Note this covers the EINTR
* case and other failures).
*/
stashed_errno = errno;
close_syscall(opened_fd);
errno = stashed_errno;
return new_fd;
}
#endif /* SYSTEM_VERSION_COMPAT_ENABLED */