31 Compound File Binary (CFB)
An .msi file is a Compound File Binary file ("structured storage"): a small file system of named streams inside one file. The format is specified in [MS-CFB]; this page is the part an MSI writer needs, plus what Windows' own writer does.
31.1 Sectors#
- The file is a sequence of fixed-size sectors. Version 3 uses 512-byte sectors (sector shift 9), version 4 uses 4096-byte sectors (shift 12). [spec]
- The header is at offset 0. Sector number
Nstarts at file offset(N + 1) * sector_sizein both versions; in version 4 the header therefore occupies a whole 4096-byte sector whose bytes after the header are zero. [spec] msi.dllwrites version 4 for every new database, whatever its size (20 KB to 10 MB were tried), with minor version0x003E. Either version is accepted when installing; rubrapack writes version 4 by default and can write version 3. [observed]
Special sector numbers in chains and tables: [spec]
| Value | Meaning |
|---|---|
FFFFFFFA | largest regular sector number |
FFFFFFFC | DIFAT sector (in the FAT) |
FFFFFFFD | FAT sector (in the FAT) |
FFFFFFFE | end of chain |
FFFFFFFF | free sector / no stream |
31.2 Header (first 512 bytes) [spec]#
| Offset | Size | Field | Value written |
|---|---|---|---|
| 0 | 8 | signature | D0 CF 11 E0 A1 B1 1A E1 |
| 8 | 16 | CLSID | zero |
| 24 | 2 | minor version | 003E |
| 26 | 2 | major version | 3 or 4 |
| 28 | 2 | byte order | FFFE (bytes FE FF) |
| 30 | 2 | sector shift | 9 or 12 |
| 32 | 2 | mini sector shift | 6 (64-byte mini sectors) |
| 34 | 6 | reserved | zero |
| 40 | 4 | number of directory sectors | 0 in version 3; the count in version 4 |
| 44 | 4 | number of FAT sectors | |
| 48 | 4 | first directory sector | |
| 52 | 4 | transaction signature | 0 |
| 56 | 4 | mini stream cutoff | 4096 |
| 60 | 4 | first mini FAT sector | FFFFFFFE if none |
| 64 | 4 | number of mini FAT sectors | |
| 68 | 4 | first DIFAT sector | FFFFFFFE if none |
| 72 | 4 | number of DIFAT sectors | |
| 76 | 436 | first 109 FAT sector numbers (DIFAT) | unused entries FFFFFFFF |
31.3 FAT, DIFAT, mini FAT [spec]#
- The FAT is an array of u32, one entry per sector, giving the next sector of the chain the sector belongs to. FAT sectors themselves are marked
FFFFFFFD. - The FAT's own sector numbers are listed in the DIFAT: the first 109 in the header, the rest in DIFAT sectors, each holding
sector_size / 4 - 1numbers and, in its last u32, the next DIFAT sector (FFFFFFFEat the end). DIFAT sectors are markedFFFFFFFCin the FAT. - With 4096-byte sectors, 109 FAT sectors already cover about 457 MB, so real MSI files in version 4 have no DIFAT sectors. With 512-byte sectors DIFAT starts at about 7 MB.
- Streams smaller than the cutoff (4096 bytes) live in the mini stream, in 64-byte mini sectors chained through the mini FAT (same layout as the FAT). The mini stream is itself stored as the root directory entry's own regular stream.
The FAT must cover its own sectors and the DIFAT's; a writer computes the counts by iterating until they stop changing.
31.4 Directory [spec]#
The directory is a chain of sectors holding 128-byte entries. Entry 0 is the root.
| Offset | Size | Field |
|---|---|---|
| 0 | 64 | name, UTF-16LE, NUL-terminated (at most 31 characters + NUL) |
| 64 | 2 | name length in bytes, including the NUL |
| 66 | 1 | type: 0 unused, 1 storage, 2 stream, 5 root |
| 67 | 1 | colour: 0 red, 1 black |
| 68 | 4 | left sibling (FFFFFFFF = none) |
| 72 | 4 | right sibling |
| 76 | 4 | child (storages and root only) |
| 80 | 16 | CLSID |
| 96 | 4 | state bits |
| 100 | 8 | creation time |
| 108 | 8 | modification time |
| 116 | 4 | starting sector (regular or mini, by size) |
| 120 | 8 | stream size (in version 3 only the low 32 bits count) |
- The root entry is named
Root Entry; its starting sector and size describe the mini stream. - The root entry of an MSI carries the CLSID
{000C1084-0000-0000-C000-000000000046}, stored as bytes84 10 0C 00 00 00 00 00 C0 00 00 00 00 00 00 46. [spec] [observed] - Unused entries are all zero except the three link fields, which are
FFFFFFFF. [spec] - The children of a storage form a red-black tree ordered first by name length, then by comparing the upper-cased UTF-16 names. [MS-CFB] allows the simplest valid tree: every node black, the tree a plain balanced binary search tree. rubrapack builds that - a balanced tree over the sorted names, all nodes black - and
msi.dllopens it. [spec] [observed] - Times may be zero. Writing zero times (and no other clock input) is what makes a CFB writer deterministic. [spec]
31.5 A simple, deterministic writer#
rubrapack lays the file out in this order, all runs contiguous:
header | FAT sectors | DIFAT sectors | directory | mini FAT | mini stream | large streams in order
- Sort stream sizes into "mini" (< 4096) and "regular".
- Mini streams are packed back to back in 64-byte units; the mini stream length is the sum.
- Count directory, mini FAT and mini stream sectors, then FAT and DIFAT sectors (iterate).
- Fill FAT chains for each contiguous run, write the directory tree, mini FAT, FAT, DIFAT, header.
31.6 Reading safely#
A reader must refuse, rather than follow: chains that loop (a chain never reaching FFFFFFFE within the number of sectors in the file), sector numbers beyond the file, a chain shorter than the stream size needs, directory links out of range, sibling trees that visit a node twice, and counts beyond sane limits. None of this costs much, and a crafted .msi otherwise hangs or crashes the reader.
31.7 Worked example: the tutorial's hello.msi#
The first package of the tutorial (chapter 2), built with --reproducible, is 139264 bytes: the header sector and 33 sectors of 4096 bytes. Its header:
| Offset | Bytes | Field | Value |
|---|---|---|---|
0x00 | d0 cf 11 e0 a1 b1 1a e1 | signature | Compound File |
0x18 | 3e 00 | minor version | 0x003E |
0x1A | 04 00 | major version | 4 |
0x1C | fe ff | byte order | 0xFFFE |
0x1E | 0c 00 | sector shift | 12: 2^12 = 4096 |
0x20 | 06 00 | mini sector shift | 6: 64 |
0x28 | 01 00 00 00 | directory sectors | 1 |
0x2C | 01 00 00 00 | FAT sectors | 1 |
0x30 | 01 00 00 00 | first directory sector | 1 |
0x38 | 00 10 00 00 | mini stream cutoff | 4096 |
0x3C | 02 00 00 00 | first mini FAT sector | 2 |
0x40 | 01 00 00 00 | mini FAT sectors | 1 |
0x44 | fe ff ff ff | first DIFAT sector | FFFFFFFE |
0x4C | 00 00 00 00 | DIFAT[0]: first FAT sector | 0 |
The sectors, and the FAT entry of each (the next sector of its chain):
| Sector | At offset | FAT entry | Holds |
|---|---|---|---|
| 0 | 0x1000 | FFFFFFFD | FAT |
| 1 | 0x2000 | FFFFFFFE | directory |
| 2 | 0x3000 | FFFFFFFE | mini FAT |
| 3 | 0x4000 | 4 | mini stream |
| 4 | 0x5000 | 5 | mini stream |
| 5 | 0x6000 | FFFFFFFE | mini stream |
| 6 | 0x7000 | 7 | Binary.RpCa |
| 7 | 0x8000 | 8 | Binary.RpCa |
| 8 | 0x9000 | 9 | Binary.RpCa |
| 9 | 0xA000 | 10 | Binary.RpCa |
| 10 | 0xB000 | 11 | Binary.RpCa |
| 11 | 0xC000 | 12 | Binary.RpCa |
| 12 | 0xD000 | 13 | Binary.RpCa |
| 13 | 0xE000 | 14 | Binary.RpCa |
| 14 | 0xF000 | 15 | Binary.RpCa |
| 15 | 0x10000 | 16 | Binary.RpCa |
| 16 | 0x11000 | 17 | Binary.RpCa |
| 17 | 0x12000 | 18 | Binary.RpCa |
| 18 | 0x13000 | 19 | Binary.RpCa |
| 19 | 0x14000 | 20 | Binary.RpCa |
| 20 | 0x15000 | 21 | Binary.RpCa |
| 21 | 0x16000 | 22 | Binary.RpCa |
| 22 | 0x17000 | 23 | Binary.RpCa |
| 23 | 0x18000 | 24 | Binary.RpCa |
| 24 | 0x19000 | 25 | Binary.RpCa |
| 25 | 0x1A000 | 26 | Binary.RpCa |
| 26 | 0x1B000 | 27 | Binary.RpCa |
| 27 | 0x1C000 | 28 | Binary.RpCa |
| 28 | 0x1D000 | 29 | Binary.RpCa |
| 29 | 0x1E000 | 30 | Binary.RpCa |
| 30 | 0x1F000 | FFFFFFFE | Binary.RpCa |
| 31 | 0x20000 | 32 | cab1.cab |
| 32 | 0x21000 | FFFFFFFE | cab1.cab |
The root directory entry, the first 128 bytes of sector 1:
| Offset | Bytes | Field | Value |
|---|---|---|---|
| 0 | 52 00 6f 00 6f 00 74 00 20 00 45 00 6e 00 74 00 72 00 79 00 ... | name | Root Entry (UTF-16LE) |
| 64 | 16 00 | name length | 22 |
| 66 | 05 | type | 5 (root) |
| 67 | 01 | colour | 1 (black) |
| 76 | 07 00 00 00 | child | entry 7 |
| 80 | 84 10 0c 00 00 00 00 00 c0 00 00 00 00 00 00 46 | CLSID | {000C1084-0000-0000-C000-000000000046} |
| 116 | 03 00 00 00 | start | sector 3 |
| 120 | 00 22 00 00 00 00 00 00 | size | 8704 |
The root's size is the mini stream's: 8704 bytes, holding 21 of the 23 streams in 64-byte mini sectors; 2 streams are large enough for regular sectors. The MSI database inside the compound file continues with the streams' names and contents.