/*-
* Copyright (c) 2024 Keve Müller <kevemueller@users.github.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <errno.h>
#include "private/binfmt.h"
#include "private/binfmt_macho.h"
#include "private/pkg.h"
#include "private/event.h"
#include "private/pkg_abi.h"
/**
* Routines to support pkg_abi.c functions when dealing with Mach-O files.
* Supports getting struct pkg_abi from the binary's load commands.
* Supports getting shared library information (needed, provided & loader).
* Picks right binary in Universal binary based on ABI.
*/
static enum pkg_arch
cputype_to_pkg_arch(const cpu_type_subtype_t cpu)
{
switch (cpu.type) {
case CPU_TYPE_ARM:
if (cpu.type_is64_32)
return (PKG_ARCH_UNKNOWN); /* aarch64-x32 */
if (cpu.type_is64)
return (PKG_ARCH_AARCH64);
switch (cpu.subtype_arm) {
case CPU_SUBTYPE_ARM_V7:
case CPU_SUBTYPE_ARM_V7S:
case CPU_SUBTYPE_ARM_V7K:
case CPU_SUBTYPE_ARM_V7M:
case CPU_SUBTYPE_ARM_V7EM:
return (PKG_ARCH_ARMV7);
case CPU_SUBTYPE_ARM_V6:
case CPU_SUBTYPE_ARM_V6M:
return (PKG_ARCH_ARMV6);
case CPU_SUBTYPE_ARM_XSCALE:
case CPU_SUBTYPE_ARM_V5:
case CPU_SUBTYPE_ARM_V4T:
case CPU_SUBTYPE_ARM_ALL:
default:
return (PKG_ARCH_UNKNOWN);
}
case CPU_TYPE_POWERPC:
if (cpu.type_is64_32)
return (PKG_ARCH_UNKNOWN); /* powerpc64-x32 */
if (cpu.type_is64)
return (PKG_ARCH_POWERPC64);
return (PKG_ARCH_POWERPC);
case CPU_TYPE_X86:
if (cpu.type_is64_32)
return (PKG_ARCH_UNKNOWN); /* amd64-x32 */
if (cpu.type_is64)
return (PKG_ARCH_AMD64);
return (PKG_ARCH_I386);
default:
return (PKG_ARCH_UNKNOWN);
}
}
static cpu_type_subtype_t
pkg_arch_to_cputype(enum pkg_arch arch) {
cpu_type_subtype_t cpu = { 0 };
switch (arch) {
case PKG_ARCH_AARCH64:
cpu.type = CPU_TYPE_ARM;
cpu.type_is64 = true;
break;
case PKG_ARCH_AMD64:
cpu.type = CPU_TYPE_X86;
cpu.type_is64 = true;
cpu.subtype_x86 = CPU_SUBTYPE_X86_ALL;
break;
case PKG_ARCH_ARMV6:
cpu.type = CPU_TYPE_ARM;
cpu.subtype_arm = CPU_SUBTYPE_ARM_V6;
break;
case PKG_ARCH_ARMV7:
cpu.type = CPU_TYPE_ARM;
cpu.subtype_arm = CPU_SUBTYPE_ARM_V7;
break;
case PKG_ARCH_I386:
cpu.type = CPU_TYPE_X86;
cpu.subtype_x86 = CPU_SUBTYPE_X86_ALL;
break;
case PKG_ARCH_POWERPC:
cpu.type = CPU_TYPE_POWERPC;
cpu.subtype_ppc = CPU_SUBTYPE_POWERPC_ALL;
break;
case PKG_ARCH_POWERPC64:
cpu.type = CPU_TYPE_POWERPC;
cpu.type_is64 = true;
cpu.subtype_ppc = CPU_SUBTYPE_POWERPC_ALL;
break;
case PKG_ARCH_ANY:
case PKG_ARCH_POWERPC64LE:
case PKG_ARCH_RISCV32:
case PKG_ARCH_RISCV64:
case PKG_ARCH_UNKNOWN:
cpu.type = CPU_TYPE_ANY;
break;
}
return cpu;
}
/**
* Using the passed mf descriptor, match the best entry using the provided hint.
* No hint or no architecture in hint -> first entry. Debug1 warning if this is not precise match (there were multiple to choose from)
* Hint -> always match, even if single architecture in file. Notice if match fails and return null.
*/
static const fat_arch_t *
match_entry(macho_file_t *mf, enum pkg_arch arch_hint)
{
const fat_arch_t *p = mf->arch;
if (arch_hint != PKG_ARCH_UNKNOWN) {
const cpu_type_subtype_t cpu_hint = pkg_arch_to_cputype(arch_hint);
const fat_arch_t *p_end = p + mf->narch;
while (p < p_end) {
// do not match cpu_hint.type == CPU_TYPE_ANY which is used if the
// arch_hint was not recognized
if (p->cpu.type == cpu_hint.type &&
p->cpu.type_is64 == cpu_hint.type_is64) {
switch (cpu_hint.type) {
case CPU_TYPE_ARM:
if (p->cpu.subtype_arm ==
CPU_SUBTYPE_ARM_ALL ||
cpu_hint.subtype_arm ==
CPU_SUBTYPE_ARM_ALL ||
p->cpu.subtype_arm ==
cpu_hint.subtype_arm) {
return p;
}
break;
case CPU_TYPE_POWERPC:
if (p->cpu.subtype_ppc ==
CPU_SUBTYPE_POWERPC_ALL ||
cpu_hint.subtype_ppc ==
CPU_SUBTYPE_POWERPC_ALL ||
p->cpu.subtype_ppc ==
cpu_hint.subtype_ppc) {
return p;
}
break;
case CPU_TYPE_X86:
if (p->cpu.subtype_x86 ==
CPU_SUBTYPE_X86_ALL ||
cpu_hint.subtype_x86 ==
CPU_SUBTYPE_X86_ALL ||
p->cpu.subtype_x86 ==
cpu_hint.subtype_x86) {
return p;
}
break;
default:
break;
}
}
pkg_debug(1, "Looking for %s, did not match %s",
pkg_arch_to_string(PKG_OS_DARWIN, arch_hint),
pkg_arch_to_string(PKG_OS_DARWIN, cputype_to_pkg_arch(p->cpu)));
p++;
}
pkg_emit_notice("Scanned %d entr%s, found none matching selector %s",
mf->narch, mf->narch > 1 ? "ies" : "y",
pkg_arch_to_string(PKG_OS_DARWIN, arch_hint));
return 0;
}
if (mf->narch > 1)
pkg_debug(1,"Found %"PRIu32" entries in universal binary, picking first",
mf->narch);
return p;
}
/**
* With a not-null, potentially pre-populated os_info structure, fill
* all members of os_info except altabi with values obtained by parsing the Mach-O
* file passed with file descriptor.
*
* The arch_hint is used to determine the fat entry to be parsed in a universal
* binary. If arch_hint is PKG_ARCH_UNKNOWN, the first entry is used.
*
* Returns EPKG_OK if all went fine, EPKG_FATAL if anything went wrong.
* Seeks the file descriptor to an arbitrary position.
*/
int
pkg_macho_abi_from_fd(int fd, struct pkg_abi *abi, enum pkg_arch arch_hint)
{
*abi = (struct pkg_abi){0};
ssize_t x;
pkg_error_t ret = EPKG_FATAL;
macho_file_t *mf = 0;
build_version_t *bv = 0;
if ((x = read_macho_file(fd, &mf)) < 0) {
goto cleanup;
}
const fat_arch_t *p = match_entry(mf, arch_hint);
if (!p) {
goto cleanup;
}
if (-1 == (x = lseek(fd, p->offset, SEEK_SET))) {
goto cleanup;
}
size_t n = 0;
macho_header_t mh;
if ((x = read_macho_header(fd, &mh)) < 0) {
goto cleanup;
}
const bool swap = mh.swap;
n = 0;
for (uint32_t ui = mh.ncmds; ui-- > 0;) {
size_t n0 = n;
uint32_t loadcmdtype;
uint32_t loadcmdsize;
READ(u32, loadcmdtype);
READ(u32, loadcmdsize);
enum MachOLoadCommand loadcmd = loadcmdtype & ~LC_REQ_DYLD;
switch (loadcmd) {
case LC_BUILD_VERSION:
if (bv) { // overwrite previous LC_VERSION_MIN_X
// values
free(bv);
bv = 0;
}
READ(build_version, bv);
break;
case LC_VERSION_MIN_IPHONEOS:
case LC_VERSION_MIN_MACOSX:
case LC_VERSION_MIN_TVOS:
case LC_VERSION_MIN_WATCHOS:
if (!bv) {
if ((x = read_min_version(fd, swap, loadcmd,
&bv)) < 0) {
goto cleanup;
}
n += x;
break;
}
// have seen the more precise
// LC_BUILD_VERSION already
// fall through and disregard this
default:
break;
}
const uint32_t fill = loadcmdsize - (n - n0);
if (fill && -1 == (x = lseek(fd, fill, SEEK_CUR))) {
goto cleanup;
}
n += fill;
if (n > mh.sizeofcmds) {
// we passed the frame boundary of the load commands
pkg_emit_error("Mach-O structure misread.");
errno = EINVAL;
goto cleanup;
}
}
if (bv) {
macho_version_t darwin;
map_platform_to_darwin(&darwin, bv->platform, bv->minos);
abi->os = PKG_OS_DARWIN;
abi->major = darwin.major;
abi->minor = darwin.minor;
abi->patch = darwin.patch;
abi->arch = cputype_to_pkg_arch(mh.cpu);
if (abi->arch == PKG_ARCH_UNKNOWN) {
ret = EPKG_FATAL;
} else {
ret = EPKG_OK;
}
} else {
pkg_emit_notice("No OS version information found in binary.");
ret = EPKG_WARN;
}
cleanup:
free(bv);
free(mf);
return ret;
}
static int
analyse_macho(int fd, struct pkg *pkg, char **provided,
enum pkg_shlib_flags *provided_flags)
{
assert(*provided == NULL);
assert(*provided_flags == PKG_SHLIB_FLAGS_NONE);
ssize_t x;
pkg_error_t ret = EPKG_END;
macho_file_t *mf = 0;
if ((x = read_macho_file(fd, &mf)) < 0) {
goto cleanup;
}
const fat_arch_t *p = match_entry(mf, ctx.abi.arch);
if (!p) {
goto cleanup;
}
if (-1 == (x = lseek(fd, p->offset, SEEK_SET))) {
goto cleanup;
}
size_t n = 0;
macho_header_t mh;
if ((x = read_macho_header(fd, &mh)) < 0) {
goto cleanup;
}
const bool swap = mh.swap;
n = 0;
for (uint32_t ui = mh.ncmds; ui-- > 0;) {
size_t n0 = n;
uint32_t loadcmdtype;
uint32_t loadcmdsize;
READ(u32, loadcmdtype);
READ(u32, loadcmdsize);
enum MachOLoadCommand loadcmd = loadcmdtype & ~LC_REQ_DYLD;
switch (loadcmd) {
case LC_RPATH:
case LC_LOAD_DYLINKER:;
char *dylinker = 0;
if ((x = read_path(fd, swap, loadcmdsize,
&dylinker)) < 0) {
goto cleanup;
}
n += x;
pkg_debug(3, "load_dylinker %d: %s\n", loadcmd, dylinker);
free(dylinker);
break;
case LC_ID_DYLIB: // provides
case LC_LOAD_DYLIB: // requires...
case LC_LOAD_WEAK_DYLIB:
case LC_REEXPORT_DYLIB:
case LC_LAZY_LOAD_DYLIB:
case LC_LOAD_UPWARD_DYLIB:;
dylib_t *dylib = 0;
if ((x = read_dylib(fd, swap, loadcmdsize,
&dylib)) < 0) {
goto cleanup;
}
n += x;
// while under Darwin full path references are recommended and ubiquitous,
// we align with pkg native environment and use only the basename
// this also strips off any @executable_path, @loader_path, @rpath components
const char * basename = strrchr(dylib->path, '/');
basename = basename ? basename + 1 : dylib->path;
pkg_debug(3,
"Adding dynamic library path: %s ts %"PRIu32" current(%"PRIuFAST16", %"PRIuFAST16", %"PRIuFAST16") compat(%"PRIuFAST16", %"PRIuFAST16", %"PRIuFAST16")\n",
dylib->path, dylib->timestamp,
dylib->current_version.major,
dylib->current_version.minor,
dylib->current_version.patch,
dylib->compatibility_version.major,
dylib->compatibility_version.minor,
dylib->compatibility_version.patch);
char *lib_with_version;
if (dylib->current_version.patch) {
xasprintf(&lib_with_version, "%s-%"PRIuFAST16".%"PRIuFAST16".%"PRIuFAST16, basename, dylib->current_version.major, dylib->current_version.minor, dylib->current_version.patch);
} else {
xasprintf(&lib_with_version, "%s-%"PRIuFAST16".%"PRIuFAST16, basename, dylib->current_version.major, dylib->current_version.minor);
}
if (LC_ID_DYLIB == loadcmd) {
if (*provided != NULL) {
pkg_emit_error("malformed Macho-O file has multiple LC_ID_DYLIB entries");
goto cleanup;
}
*provided = xstrdup(lib_with_version);
} else {
pkg_addshlib_required(pkg, lib_with_version, PKG_SHLIB_FLAGS_NONE);
}
free(lib_with_version);
free(dylib);
break;
default:
break;
}
const uint32_t fill = loadcmdsize - (n - n0);
if (fill && -1 == (x = lseek(fd, fill, SEEK_CUR))) {
goto cleanup;
}
n += fill;
if (n > mh.sizeofcmds) {
// we passed the frame boundary of the load commands
pkg_emit_error("Mach-O structure misread.");
errno = EINVAL;
goto cleanup;
}
}
cleanup:
free(mf);
return ret;
}
int
pkg_analyse_init_macho(__unused const char *stage)
{
return EPKG_OK;
}
int
pkg_analyse_macho(const bool developer_mode, struct pkg *pkg,
const char *fpath, char **provided, enum pkg_shlib_flags *provided_flags)
{
assert(*provided == NULL);
assert(*provided_flags == PKG_SHLIB_FLAGS_NONE);
int ret = EPKG_OK;
pkg_debug(1, "Analysing Mach-O %s", fpath);
int fd = open(fpath, O_RDONLY);
if (-1 == fd) {
// pkg_emit_errno("open_pkg_analyse_macho", fpath);
// ret = EPKG_FATAL;
// Be consistent with analyse_elf and return no error if fpath cannot be opened
return ret;
} else {
ret = analyse_macho(fd, pkg, provided, provided_flags);
if (-1 == close(fd)) {
pkg_emit_errno("close_pkg_analyse_macho", fpath);
ret = EPKG_FATAL;
}
}
if (developer_mode) {
if (ret != EPKG_OK && ret != EPKG_END) {
return EPKG_WARN;
}
}
return ret;
}
int
pkg_analyse_close_macho(void)
{
return EPKG_OK;
}