ByteScope

Mach-O (universal/fat) file format

A macOS universal (fat) Mach-O binary bundling multiple architectures.

MIME type
application/x-mach-binary
Extensions
.dylib, .bundle
Category
Executables & bytecode

Magic-number signature

0x00000000CAFEBABE····'CAFEBABE' fat magic

About the Mach-O (universal/fat) format

A universal binary is a wrapper, not a binary: after an eight-byte header it carries one twenty-byte descriptor per architecture, and then the complete, independent Mach-O images themselves, each page-aligned at the offset its descriptor names. That is why a universal file is roughly the sum of its slices rather than a clever merge, and why lipo can add or remove an architecture without touching any code. The whole fat header is big-endian regardless of what the slices inside it are, a NeXT-era decision that survives because the format has been through three CPU transitions unchanged.

Header layout, byte by byte

OffsetLengthFieldMeaning
04magic (FAT_MAGIC)CA FE BA BE, read big-endian as 0xCAFEBABE — the same four bytes as a Java class file. The variant CA FE BA BF (FAT_MAGIC_64) means the architecture descriptors below use 64-bit offsets and sizes, for binaries that exceed four gigabytes.
44nfat_archHow many architecture descriptors follow, big-endian. Realistically 1 to about 4 — two is overwhelmingly the common case, x86-64 plus arm64. Because the count is small, its upper two bytes are zero, which is exactly the property that separates this header from a Java class file.
84fat_arch[0].cputypeCPU of the first slice: 0x01000007 x86-64, 0x0100000C arm64, and without the 0x01000000 flag the 32-bit forms. This is the field a build system reads to answer whether the file supports the machine in front of it.
124fat_arch[0].cpusubtypeVariant within that architecture — 3 for x86-64 ALL, 0x80000002 for arm64e. Two slices may share a cputype and differ only here, which is how arm64 and arm64e can both be present.
164fat_arch[0].offsetFile offset where this slice's own Mach-O image starts, big-endian. Seek there and you should find CF FA ED FE. Everything inside the slice is measured from that point, not from the start of the fat file, which is why a slice can be copied out byte for byte and still run.
204fat_arch[0].sizeByte length of that slice. Offset plus size for the last architecture is normally the file size; anything after it is padding or an appended signature.
244fat_arch[0].alignAlignment as a power of two — 14 means the slice starts on a 16384-byte boundary, which is the page size on Apple silicon. This is why there are large runs of zero bytes between slices and why a universal binary is slightly larger than the sum of its parts.
2820fat_arch[1]The next descriptor, same five fields in the same order. Descriptors are fixed at twenty bytes each (thirty-two in the FAT_MAGIC_64 form), so the nth one starts at 8 + 20n and there is no list terminator — nfat_arch is the only bound.

Telling it apart from its neighbours

Easily confused withHow to tell
Java class fileThe collision that has confused magic-number tables for thirty years: both start CA FE BA BE. Read bytes 4-7 as a 16-bit pair. A Java class file has minor_version then major_version, and major_version is at least 45 for any real class file. A fat binary has nfat_arch there, a small count, so the bytes where major_version would sit are zero. Checking for a nonzero major_version first resolves it cleanly.
Mach-O executableA thin Mach-O begins with CF FA ED FE or CE FA ED FE and its header fields are little-endian; the fat wrapper begins CA FE BA BE and every field is big-endian. If you extract a slice and inspect it separately, it is a thin binary — the fat header does not survive the copy, which is the correct behaviour.
FAT_MAGIC_64 (CAFEBABF)One bit apart from the 32-bit form, and mixing them up is silent rather than loud: the descriptors are thirty-two bytes rather than twenty, with 64-bit offset and size. A parser assuming the short layout reads the second architecture's fields from the middle of the first descriptor and reports plausible garbage.
Java JARWorth stating because it is how the collision usually reaches a person: a JAR is a ZIP (50 4B 03 04), and the CA FE BA BE inside it is a compressed class file rather than anything Apple-related. Whatever appears at offset 0 is what counts.

How these files break

  • A truncated final slice. The fat header is at the front and lists every architecture with its offset and size, so a partial download still reports 'universal binary with 2 architectures' while the last slice's bytes stop early. Compare the last descriptor's offset plus size against the real file size.
  • Descriptors that no longer match the slices. Editing a slice in place and changing its length without updating the fat header leaves the following architecture's offset pointing into the middle of the previous one, and the second slice's magic no longer reads CF FA ED FE. Every slice offset should land on a Mach-O magic.
  • Prepended junk. An error page or a BOM saved ahead of the binary shifts every slice offset by the same amount, so the file still identifies as CAFEBABE-something while nothing inside it resolves. The fix is deleting everything before the magic, not repairing the descriptors.
  • A code signature invalidated per slice. Each slice is signed independently, so patching one architecture leaves the others valid and produces a binary that runs on an Intel Mac and is killed on Apple silicon, or vice versa — the confusing symptom that sends people looking for a hardware problem.

Where the format stands today

Universal binaries are the mechanism Apple uses to get through CPU transitions without shipping two downloads: 68k to PowerPC, PowerPC to Intel, and Intel to Apple silicon each ran on this same header. Today a universal file usually means x86-64 plus arm64, which is what most Mac apps and Homebrew bottles that predate Apple-silicon-only builds contain, and what Xcode produces when a project targets both. The cost is size — the file carries complete copies of the program — so App Store delivery thins them before download and most in-house tools now ship arm64 only. The format itself is unlikely to disappear even so, because it is also how arm64 and arm64e images coexist in one file, and because lipo makes adding or stripping an architecture a header rewrite rather than a rebuild.

Open Mach-O (universal/fat) files on this site

Every one of these runs in your browser — the file never leaves your machine.

Related formats

Formats that are easy to confuse with Mach-O (universal/fat) — compare their leading bytes.

FormatFirst bytesExtension
Mach-O executableCF FA ED FE.dylib
Java class fileCA FE BA BE.class
ELF executable7F 45 4C 46.elf

Frequently asked questions

What is a magic number in a file?

The first few bytes of a file, which identify its format regardless of what the name says. A PNG always starts with 89 50 4E 47; a ZIP with 50 4B 03 04. Operating systems, browsers and the file command all read these bytes rather than trusting the extension, which is why renaming a file does not change what it actually is.

How do I check a file's magic number myself?

On macOS or Linux, xxd -l 16 yourfile prints the first 16 bytes; on Windows, Format-Hex -Path yourfile -Count 16 in PowerShell. Or drop the file into the File Type Identifier on this site, which reads the bytes locally and names the format for you — nothing is uploaded.

Why do two formats share the same magic number?

Because one is built on the other. Every docx, xlsx, pptx, apk, jar and epub begins with the ZIP signature, since all of them are ZIP archives underneath; telling them apart means looking at the entries inside. Java class files and macOS universal binaries both start with CA FE BA BE and are distinguished by the field that follows.

Can a file have no magic number at all?

Yes. Text-based formats — XML, HTML, JSON, SVG, CSV — have no fixed byte signature, so they are recognized structurally instead: a leading <?xml declaration, a <svg element, or a { followed by a quoted key. That detection is a good guess rather than a certainty, which is why this reference marks those formats as probe-detected.

Does a matching magic number mean the file is safe?

No. It tells you what the format is, not whether the contents are harmless — a genuine PDF or docx can still carry a malicious payload. Identifying the true type is a useful first check, especially when the extension disagrees with the bytes, but it is not a malware scan.

Have a file that might be in the Mach-O (universal/fat) format? Identify it with the File Type Identifier — it reads the magic number locally, nothing is uploaded. Or browse all formats.