master ohmu/fastfetch-openbsd / src / common / impl / binary_apple.c
ohmu  ·  2026-10-15
  1#include "common/binary.h"
  2#include "common/debug.h"
  3#include "common/io.h"
  4#include "common/strutil.h"
  5
  6#include <errno.h>
  7#include <string.h>
  8#include <fcntl.h>
  9#include <sys/mman.h>
 10#include <mach-o/loader.h>
 11#include <mach-o/swap.h>
 12#include <mach-o/fat.h>
 13
 14#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // swap_fat_arch
 15
 16// Ref: https://github.com/AlexDenisov/segment_dumper/blob/master/main.c
 17
 18typedef struct {
 19    const uint8_t* data;
 20    size_t length;
 21} FFMemoryMapping;
 22
 23static inline void wrapMunmap(FFMemoryMapping* mapping) {
 24    assert(mapping);
 25    if (mapping->data == nullptr || mapping->data == MAP_FAILED) {
 26        return;
 27    }
 28    munmap((void*) mapping->data, mapping->length);
 29}
 30
 31/**
 32 * Helper function to access data from a memory-mapped file at a specific offset
 33 */
 34static inline const void* readData(const FFMemoryMapping* mapping, size_t size, off_t offset) {
 35    if (offset < 0) {
 36        return nullptr;
 37    }
 38
 39    size_t start = (size_t) offset;
 40    if (start > mapping->length || size > mapping->length - start) {
 41        return nullptr;
 42    }
 43
 44    return mapping->data + start;
 45}
 46
 47/**
 48 * Handles a Mach-O section by extracting strings from the __cstring section
 49 *
 50 * @param mapping Memory mapping of the Mach-O object file
 51 * @param name Section name to check
 52 * @param offset Offset of the section in the file
 53 * @param size Size of the section
 54 * @param cb Callback function to process strings
 55 * @param userdata User data for the callback
 56 * @param minLength Minimum string length to extract
 57 *
 58 * @return true to continue processing, false to stop
 59 */
 60static bool handleMachSection(const FFMemoryMapping* mapping, const char* name, off_t offset, size_t size, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
 61    if (!ffStrEquals(name, "__cstring")) {
 62        return true;
 63    }
 64
 65    const char* data = readData(mapping, size, offset);
 66    if (!data) {
 67        return true;
 68    }
 69
 70    for (size_t off = 0; off < size; ++off) {
 71        const char* p = data + off;
 72        if (*p == '\0') {
 73            continue;
 74        }
 75        uint32_t len = (uint32_t) strnlen(p, size - off);
 76        if (len < minLength) {
 77            off += len; // Skip short strings
 78            continue;
 79        }
 80        if (*p >= ' ' && *p <= '~') { // Ignore control characters
 81            if (!cb(p, len, userdata)) {
 82                return false;
 83            }
 84        }
 85        off += len;
 86    }
 87    return true;
 88}
 89
 90/**
 91 * Processes a Mach-O header (32-bit or 64-bit)
 92 *
 93 * This function parses the load commands in a Mach-O header, looking for
 94 * LC_SEGMENT or LC_SEGMENT_64 commands that contain the __TEXT segment.
 95 * It then processes the sections within that segment to extract strings.
 96 *
 97 * @param mapping Memory mapping of the Mach-O object file
 98 * @param offset Offset of the Mach header in the file
 99 * @param is_64 Whether this is a 64-bit Mach-O header
100 * @param cb Callback function to process strings
101 * @param userdata User data for the callback
102 * @param minLength Minimum string length to extract
103 *
104 * @return nullptr on success, error message on failure
105 */
106static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset, bool is_64, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
107    uint32_t ncmds;
108    off_t loadCommandsOffset = offset;
109
110    if (is_64) {
111        const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset);
112        if (!header) {
113            FF_DEBUG("Failed to read the 64-bit mach header at offset %lld", (long long) offset);
114            return "read mach header failed";
115        }
116
117        ncmds = header->ncmds;
118        loadCommandsOffset += sizeof(*header);
119    } else {
120        const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset);
121        if (!header) {
122            FF_DEBUG("Failed to read the mach header at offset %lld", (long long) offset);
123            return "read mach header failed";
124        }
125
126        ncmds = header->ncmds;
127        loadCommandsOffset += sizeof(*header);
128    }
129
130    off_t commandOffset = loadCommandsOffset;
131    const struct load_command* cmd = nullptr;
132    for (uint32_t i = 0U; i < ncmds; i++, commandOffset += cmd->cmdsize) {
133        cmd = readData(mapping, sizeof(*cmd), commandOffset);
134        if (!cmd) {
135            break;
136        }
137
138        if (cmd->cmdsize < sizeof(*cmd)) {
139            break;
140        }
141
142        if (cmd->cmd == LC_SEGMENT_64) {
143            const struct segment_command_64* segment = readData(mapping, sizeof(struct segment_command_64), commandOffset);
144            if (!segment) {
145                continue;
146            }
147
148            if (!ffStrEquals(segment->segname, "__TEXT")) {
149                continue;
150            }
151
152            for (uint32_t j = 0U; j < segment->nsects; j++) {
153                off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section_64));
154                const struct section_64* section = readData(mapping, sizeof(struct section_64), sectionOffset);
155                if (!section) {
156                    continue;
157                }
158
159                if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
160                    return nullptr;
161                }
162            }
163        } else if (cmd->cmd == LC_SEGMENT) {
164            const struct segment_command* segment = readData(mapping, sizeof(struct segment_command), commandOffset);
165            if (!segment) {
166                continue;
167            }
168
169            if (!ffStrEquals(segment->segname, "__TEXT")) {
170                continue;
171            }
172
173            for (uint32_t j = 0; j < segment->nsects; j++) {
174                off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section));
175                const struct section* section = readData(mapping, sizeof(struct section), sectionOffset);
176                if (!section) {
177                    continue;
178                }
179
180                if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
181                    return nullptr;
182                }
183            }
184        }
185    }
186
187    return nullptr;
188}
189
190/**
191 * Processes a Fat binary header (Universal binary)
192 *
193 * This function handles the fat header of a universal binary, which can contain
194 * multiple Mach-O binaries for different architectures. It extracts and processes
195 * each embedded Mach-O file.
196 *
197 * @param mapping Memory mapping of the universal binary
198 * @param cb Callback function to process strings
199 * @param userdata User data for the callback
200 * @param minLength Minimum string length to extract
201 *
202 * @return nullptr on success, error message on failure
203 */
204static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
205    const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0);
206    if (!headerRaw) {
207        FF_DEBUG("Failed to read the fat header");
208        return "read fat header failed";
209    }
210
211    struct fat_header header = *headerRaw;
212
213    bool needSwap = header.magic == FAT_CIGAM || header.magic == FAT_CIGAM_64;
214
215    if (needSwap) {
216        swap_fat_header(&header, NX_UnknownByteOrder);
217    }
218
219    for (uint32_t i = 0U; i < header.nfat_arch; i++) {
220        off_t machHeaderOffset = 0;
221        if (header.magic == FAT_MAGIC) {
222            off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch));
223            const struct fat_arch* archRaw = readData(mapping, sizeof(struct fat_arch), archOffset);
224            if (!archRaw) {
225                continue;
226            }
227
228            struct fat_arch arch = *archRaw;
229
230            if (needSwap) {
231                swap_fat_arch(&arch, 1, NX_UnknownByteOrder);
232            }
233            machHeaderOffset = (off_t) arch.offset;
234        } else {
235            off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch_64));
236            const struct fat_arch_64* archRaw = readData(mapping, sizeof(struct fat_arch_64), archOffset);
237            if (!archRaw) {
238                continue;
239            }
240
241            struct fat_arch_64 arch = *archRaw;
242
243            if (needSwap) {
244                swap_fat_arch_64(&arch, 1, NX_UnknownByteOrder);
245            }
246
247            machHeaderOffset = (off_t) arch.offset;
248        }
249
250        const uint32_t* magic = readData(mapping, sizeof(uint32_t), machHeaderOffset);
251        if (!magic) {
252            continue;
253        }
254
255        if (*magic == MH_MAGIC_64 || *magic == MH_MAGIC) {
256            return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength);
257        }
258    }
259    FF_DEBUG("No supported Mach-O slice was found in the fat binary");
260    return "Unsupported fat header";
261}
262
263/**
264 * Extracts string literals from a Mach-O (Apple) binary file
265 *
266 * This function supports both single-architecture Mach-O files and
267 * universal binaries (fat binaries) containing multiple architectures.
268 * It locates the __cstring section in the __TEXT segment which contains
269 * the string literals used in the program.
270 */
271const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
272    FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC);
273    if (fd < 0) {
274        FF_DEBUG("open(\"%s\") failed: %s", machoFile, strerror(errno));
275        return "File could not be opened";
276    }
277
278    struct stat st;
279    if (fstat(fd, &st) != 0 || st.st_size <= 0) {
280        FF_DEBUG("fstat(\"%s\") failed or the file is empty", machoFile);
281        return "Failed to stat file";
282    }
283
284    [[gnu::cleanup(wrapMunmap)]] FFMemoryMapping mapping = {
285        .data = mmap(nullptr, (size_t) st.st_size, PROT_READ, MAP_PRIVATE, fd, 0),
286        .length = (size_t) st.st_size,
287    };
288    if (mapping.data == MAP_FAILED) {
289        FF_DEBUG("mmap(\"%s\", %lld bytes) failed: %s", machoFile, (long long) st.st_size, strerror(errno));
290        return "mmap failed";
291    }
292
293    // Read the magic number to determine the type of binary
294    const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0);
295    if (!magic) {
296        FF_DEBUG("Failed to read the magic number");
297        return "read magic number failed";
298    }
299
300    // Check for supported formats
301    // MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated.
302    // However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim).
303    if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) {
304        FF_DEBUG("Unsupported magic number 0x%08x", (unsigned) *magic);
305        return "Unsupported format or big endian mach-o file";
306    }
307
308    // Process either a fat binary or a regular Mach-O binary
309    if (*magic == FAT_MAGIC || *magic == FAT_MAGIC_64 || *magic == FAT_CIGAM || *magic == FAT_CIGAM_64) {
310        return dumpFatHeader(&mapping, cb, userdata, minLength);
311    } else {
312        return dumpMachHeader(&mapping, 0, *magic == MH_MAGIC_64, cb, userdata, minLength);
313    }
314}