rubrapack Manual←↑→

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#

Special sector numbers in chains and tables: [spec]

ValueMeaning
FFFFFFFAlargest regular sector number
FFFFFFFCDIFAT sector (in the FAT)
FFFFFFFDFAT sector (in the FAT)
FFFFFFFEend of chain
FFFFFFFFfree sector / no stream

31.2 Header (first 512 bytes) [spec]#

OffsetSizeFieldValue written
08signatureD0 CF 11 E0 A1 B1 1A E1
816CLSIDzero
242minor version003E
262major version3 or 4
282byte orderFFFE (bytes FE FF)
302sector shift9 or 12
322mini sector shift6 (64-byte mini sectors)
346reservedzero
404number of directory sectors0 in version 3; the count in version 4
444number of FAT sectors
484first directory sector
524transaction signature0
564mini stream cutoff4096
604first mini FAT sectorFFFFFFFE if none
644number of mini FAT sectors
684first DIFAT sectorFFFFFFFE if none
724number of DIFAT sectors
76436first 109 FAT sector numbers (DIFAT)unused entries FFFFFFFF

31.3 FAT, DIFAT, mini FAT [spec]#

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.

OffsetSizeField
064name, UTF-16LE, NUL-terminated (at most 31 characters + NUL)
642name length in bytes, including the NUL
661type: 0 unused, 1 storage, 2 stream, 5 root
671colour: 0 red, 1 black
684left sibling (FFFFFFFF = none)
724right sibling
764child (storages and root only)
8016CLSID
964state bits
1008creation time
1088modification time
1164starting sector (regular or mini, by size)
1208stream size (in version 3 only the low 32 bits count)

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
  1. Sort stream sizes into "mini" (< 4096) and "regular".
  2. Mini streams are packed back to back in 64-byte units; the mini stream length is the sum.
  3. Count directory, mini FAT and mini stream sectors, then FAT and DIFAT sectors (iterate).
  4. 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:

OffsetBytesFieldValue
0x00d0 cf 11 e0 a1 b1 1a e1signatureCompound File
0x183e 00minor version0x003E
0x1A04 00major version4
0x1Cfe ffbyte order0xFFFE
0x1E0c 00sector shift12: 2^12 = 4096
0x2006 00mini sector shift6: 64
0x2801 00 00 00directory sectors1
0x2C01 00 00 00FAT sectors1
0x3001 00 00 00first directory sector1
0x3800 10 00 00mini stream cutoff4096
0x3C02 00 00 00first mini FAT sector2
0x4001 00 00 00mini FAT sectors1
0x44fe ff ff fffirst DIFAT sectorFFFFFFFE
0x4C00 00 00 00DIFAT[0]: first FAT sector0

The sectors, and the FAT entry of each (the next sector of its chain):

SectorAt offsetFAT entryHolds
00x1000FFFFFFFDFAT
10x2000FFFFFFFEdirectory
20x3000FFFFFFFEmini FAT
30x40004mini stream
40x50005mini stream
50x6000FFFFFFFEmini stream
60x70007Binary.RpCa
70x80008Binary.RpCa
80x90009Binary.RpCa
90xA00010Binary.RpCa
100xB00011Binary.RpCa
110xC00012Binary.RpCa
120xD00013Binary.RpCa
130xE00014Binary.RpCa
140xF00015Binary.RpCa
150x1000016Binary.RpCa
160x1100017Binary.RpCa
170x1200018Binary.RpCa
180x1300019Binary.RpCa
190x1400020Binary.RpCa
200x1500021Binary.RpCa
210x1600022Binary.RpCa
220x1700023Binary.RpCa
230x1800024Binary.RpCa
240x1900025Binary.RpCa
250x1A00026Binary.RpCa
260x1B00027Binary.RpCa
270x1C00028Binary.RpCa
280x1D00029Binary.RpCa
290x1E00030Binary.RpCa
300x1F000FFFFFFFEBinary.RpCa
310x2000032cab1.cab
320x21000FFFFFFFEcab1.cab

The root directory entry, the first 128 bytes of sector 1:

OffsetBytesFieldValue
052 00 6f 00 6f 00 74 00 20 00 45 00 6e 00 74 00 72 00 79 00 ...nameRoot Entry (UTF-16LE)
6416 00name length22
6605type5 (root)
6701colour1 (black)
7607 00 00 00childentry 7
8084 10 0c 00 00 00 00 00 c0 00 00 00 00 00 00 46CLSID{000C1084-0000-0000-C000-000000000046}
11603 00 00 00startsector 3
12000 22 00 00 00 00 00 00size8704

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.