Parse ar files for Mach-O code

Signed-off-by: Paul Guyot <pguyot@kallisys.net>
This commit is contained in:
Paul Guyot
2026-06-15 18:25:29 +02:00
committed by Clemens Lang
parent 1db3e7ff4a
commit 4665f63232
5 changed files with 153 additions and 3 deletions
+16 -2
View File
@@ -50,7 +50,8 @@ clean::
rm -f ${SWIG_OBJS} ${PKG_INDEX}
rm -f ${TESTS} \
tests/libmachista-test-dependency${SHLIB_SUFFIX} \
tests/libmachista-test-lib${SHLIB_SUFFIX}
tests/libmachista-test-lib${SHLIB_SUFFIX} \
tests/libmachista-test-archive.a
rm -rf ${TESTS:%=%.dSYM} \
tests/libmachista-test-dependency${SHLIB_SUFFIX}.dSYM \
tests/libmachista-test-lib${SHLIB_SUFFIX}.dSYM
@@ -61,9 +62,22 @@ distclean::
test:: ${TESTS}
${TESTS}
tests/libmachista-test: tests/libmachista-test.c libmachista.h libmachista.o hashmap.o tests/libmachista-test-lib${SHLIB_SUFFIX}
tests/libmachista-test: tests/libmachista-test.c libmachista.h libmachista.o hashmap.o tests/libmachista-test-lib${SHLIB_SUFFIX} tests/libmachista-test-archive.a
$(CC) $(CFLAGS) -D_POSIX_SOURCE -o $@ -I. $< libmachista.o hashmap.o
# A static archive (e.g. a *.a library) of more than one host-arch object, used
# to check that libmachista parses archives and reports each arch only once.
tests/libmachista-test-archive.a: tests/empty.c
ifeq (darwin,@OS_PLATFORM@)
$(CC) $(CFLAGS) -c $< -o tests/libmachista-test-archive-1.o
$(CC) $(CFLAGS) -c $< -o tests/libmachista-test-archive-2.o
rm -f $@
ar rcs $@ tests/libmachista-test-archive-1.o tests/libmachista-test-archive-2.o
rm -f tests/libmachista-test-archive-1.o tests/libmachista-test-archive-2.o
else
touch $@
endif
# The tests for this library need a universal lib; we used to just use
# /usr/lib/libSystem.B.dylib, but Apple has removed that with macOS 11, so now
# we need a different alternative.
+82
View File
@@ -52,6 +52,8 @@
#include <strings.h>
#ifdef __MACH__
#include <ar.h>
#include <mach-o/dyld.h>
#include <mach-o/fat.h>
#include <mach-o/loader.h>
@@ -230,12 +232,19 @@ static macho_loadcmd_t *macho_loadcmdlist_append (macho_arch_t *mat) {
/* Parse a Mach-O header */
#ifdef __MACH__
static int parse_ar (macho_t *mt, macho_input_t *input);
static int parse_macho (macho_t *mt, macho_input_t *input) {
/* Read the file type. */
const uint32_t *magic = macho_read(input, input->data, sizeof(uint32_t));
if (magic == NULL)
return MACHO_ERANGE;
/* A static archive (e.g. *.a) is a container of Mach-O objects */
if (input->length >= SARMAG && memcmp(input->data, ARMAG, SARMAG) == 0) {
return parse_ar(mt, input);
}
/* Parse the Mach-O header */
bool universal = false;
uint32_t (*swap32)(uint32_t) = macho_nswap32;
@@ -430,6 +439,79 @@ static int parse_macho (macho_t *mt, macho_input_t *input) {
}
#endif
/* Parse a static archive, collecting the architectures of its Mach-O members */
#ifdef __MACH__
static int parse_ar (macho_t *mt, macho_input_t *input) {
/* Skip over the "!<arch>\n" magic */
size_t offset = SARMAG;
while (offset + sizeof(struct ar_hdr) <= input->length) {
const struct ar_hdr *hdr = macho_offset(input, input->data, offset, sizeof(struct ar_hdr));
if (hdr == NULL)
return MACHO_ERANGE;
if (memcmp(hdr->ar_fmag, ARFMAG, sizeof(hdr->ar_fmag)) != 0)
return MACHO_EMAGIC;
/* ar_size counts the extended name (if any) plus the member data */
char sizebuf[sizeof(hdr->ar_size) + 1];
memcpy(sizebuf, hdr->ar_size, sizeof(hdr->ar_size));
sizebuf[sizeof(hdr->ar_size)] = '\0';
long member_size = strtol(sizebuf, NULL, 10);
if (member_size <= 0)
return MACHO_ERANGE;
size_t data_offset = offset + sizeof(struct ar_hdr);
size_t data_size = (size_t) member_size;
/* BSD ar stores a long name as "#1/<length>" followed by the name */
size_t name_len = 0;
if (strncmp(hdr->ar_name, AR_EFMT1, sizeof(AR_EFMT1) - 1) == 0) {
name_len = (size_t) strtol(hdr->ar_name + sizeof(AR_EFMT1) - 1, NULL, 10);
if (name_len > data_size)
return MACHO_ERANGE;
}
macho_input_t member;
member.length = data_size - name_len;
member.data = macho_offset(input, input->data, data_offset + name_len, member.length);
if (member.data == NULL)
return MACHO_ERANGE;
/* non-Mach-O members (e.g. the symbol table) report EMAGIC; ignore them */
int res = parse_macho(mt, &member);
if (res != MACHO_SUCCESS && res != MACHO_EMAGIC)
return res;
/* members are padded to a two-byte boundary */
offset = data_offset + data_size;
if ((offset & 1) != 0)
offset++;
}
/* collapse the duplicate archs of the many same-arch objects in an archive */
for (macho_arch_t *a = mt->mt_archs; a != NULL; a = a->next) {
macho_arch_t *prev = a;
macho_arch_t *cur = a->next;
while (cur != NULL) {
if (cur->mat_arch == a->mat_arch) {
prev->next = cur->next;
free_macho_arch_t(cur);
cur = prev->next;
} else {
prev = cur;
cur = cur->next;
}
}
}
if (mt->mt_archs == NULL)
return MACHO_EMAGIC;
return MACHO_SUCCESS;
}
#endif
/* Parse a (possible Mach-O) file. For a more detailed description, see the header */
#ifdef __MACH__
int macho_parse_file(macho_handle_t *handle, const char *filepath, const macho_t **res) {
+43
View File
@@ -11,6 +11,7 @@
#include <unistd.h>
#define TEST_LIB_PATH "tests/libmachista-test-lib.dylib"
#define TEST_ARCHIVE_PATH "tests/libmachista-test-archive.a"
#define OTOOL_PATH "/usr/bin/otool"
// check helper
@@ -305,6 +306,47 @@ static bool test_libsystem(void) {
return false;
}
/**
* Test reading a static archive (e.g. a *.a library). The fixture holds two
* objects of the same arch, so parsing must succeed and report exactly one
* (deduplicated), valid architecture.
*/
static void forked_test_archive(void) {
macho_handle_t *handle = macho_create_handle();
const macho_t *result;
int ret;
bool ok = true;
if ((ret = macho_parse_file(handle, TEST_ARCHIVE_PATH, &result)) != MACHO_SUCCESS) {
printf("\tError parsing `%s': %s\n", TEST_ARCHIVE_PATH, macho_strerror(ret));
macho_destroy_handle(handle);
exit(EXIT_FAILURE);
}
if (result->mt_archs == NULL) {
printf("\tNo architecture reported for `%s'\n", TEST_ARCHIVE_PATH);
ok = false;
} else if (result->mt_archs->next != NULL) {
printf("\tExpected a single (deduplicated) architecture for `%s'\n", TEST_ARCHIVE_PATH);
ok = false;
} else if (macho_get_arch_name(result->mt_archs->mat_arch) == NULL) {
printf("\tUnknown architecture reported for `%s'\n", TEST_ARCHIVE_PATH);
ok = false;
}
macho_destroy_handle(handle);
exit(!ok);
}
static bool test_archive(void) {
puts("Testing parsing " TEST_ARCHIVE_PATH);
if (fork_test(forked_test_archive, "Error parsing " TEST_ARCHIVE_PATH)) {
puts("\tOK");
return true;
}
puts("\tError");
return false;
}
/**
* Test macho_format_dylib_version
*/
@@ -351,6 +393,7 @@ int main(void) {
result &= test_handle();
result &= test_format_dylib_version();
result &= test_libsystem();
result &= test_archive();
return !result;
#else
return 0;
+11
View File
@@ -80,6 +80,7 @@
#endif
#ifdef __MACH__
#include <ar.h>
#include <mach-o/loader.h>
#include <mach-o/fat.h>
#endif
@@ -559,6 +560,16 @@ static int fileIsBinaryCmd(ClientData clientData UNUSED, Tcl_Interp *interp, int
Tcl_SetObjResult(interp, Tcl_NewBooleanObj(true));
return TCL_OK;
}
if (memcmp(&magic, ARMAG, sizeof(magic)) == 0) {
/* static archive ("!<arch>\n", e.g. *.a); holds mach-o objects */
char rest[SARMAG - sizeof(magic)];
if (sizeof(rest) == fread(rest, 1, sizeof(rest), file)
&& memcmp(rest, &ARMAG[sizeof(magic)], sizeof(rest)) == 0) {
fclose(file);
Tcl_SetObjResult(interp, Tcl_NewBooleanObj(true));
return TCL_OK;
}
}
if (magic == htonl(FAT_MAGIC)) {
uint32_t archcount;
/* either universal binary or java class (FAT_MAGIC == 0xcafebabe)
+1 -1
View File
@@ -33,7 +33,7 @@ proc porttest::get_file_archs {handle fpath} {
set returncode [lindex $resultlist 0]
set result [lindex $resultlist 1]
if {$returncode != $::machista::SUCCESS} {
# fails on static libs, ignore
# EMAGIC means not Mach-O (nor an archive of Mach-O objects); ignore
if {$returncode != $::machista::EMAGIC} {
ui_warn "Error parsing file ${fpath}: [machista::strerror $returncode]"
}