Search tools

AppImage File Structure: ELF Runtime and SquashFS

A type 2 AppImage is one file in two parts: a small ELF program, the runtime, followed by a SquashFS image that holds the application’s files. The image starts exactly where the ELF file ends, and that end is computed from the ELF section headers, not found by searching for the SquashFS signature.

The two parts

The runtime is an ordinary Linux executable. When the AppImage is started, the runtime mounts the image that follows it and runs the file AppRun in the image’s root. The image is a compressed, read-only file system that contains the application, one desktop file and its icons.

The AppImage specification requires the bytes 0x41 0x49 0x02 (“AI” and the type number 2) at offset 8, inside the ELF identification at the start of the file. That marker is what tells a type 2 AppImage apart from any other Linux program without running it.

Byte map of a small synthetic type 2 AppImage, measured by UNQIRO’s ELF and SquashFS readers when this page was built. Real runtimes are much larger; the structure is the same.
Offset and length Part Contents
0
0x0000
64 bytes
ELF runtime ELF header: 7F "ELF", 64-bit; "AI" 0x02 at offset 8; section header table at 1,600, 5 entries
64
0x0040
640 bytes
ELF runtime Program headers, code and data of the runtime
704
0x02C0
4 bytes
ELF runtime The bytes "hsqs" inside the runtime’s data: not a file system
708
0x02C4
892 bytes
ELF runtime More code and data
1,600
0x0640
320 bytes
ELF runtime Section header table: 5 entries of 64 bytes
1,920
0x0780
1,024 bytes
ELF runtime Data of the last section, after the table
2,944
0x0B80
96 bytes
SquashFS image Superblock: "hsqs", version 4.0, compression ID 6 (zstd), block size 131,072
3,040
0x0BE0
160 bytes
SquashFS image Tables and compressed file data (the AppDir)

How the offset is found

The runtime computes the size of its own ELF file and treats everything after it as the file system. In type2-runtime this takes two numbers from the ELF headers: the end of the section header table, and the end of the data of the last section. The larger one is the offset.

  1. Section header table: e_shoff + e_shentsize × e_shnum = 1,600 + 64 × 5 = 1,920
  2. Last section: sh_offset + sh_size = 1,920 + 1,024 = 2,944
  3. Offset: the larger value, 2,944. The superblock there starts with hsqs.

In this example the second number decides: the table ends at 1,920, but a section's data follows it, so the image starts at 2,944. Reading only the end of the table would miss it.

The same number is what --appimage-offset prints, and the AppImage documentation uses it to mount the image with mount -o offset=. That option is handled by the runtime, so getting the number this way runs the AppImage’s runtime.

Why searching for “hsqs” is only a heuristic

A common shortcut is to search the file for hsqs, the SquashFS signature, and use the first match as the offset. It can work, but the four bytes are not reserved: nothing keeps them out of the runtime’s code or data, and a search does not know where the ELF file ends.

In the example above, a search stops at byte 704, inside the runtime. The ELF headers point to byte 2,944, where the image really begins. The runtime itself never searches.

What the image contains

The image is an AppDir: AppRun as the entry point, one desktop file in the root (the AppDir specification allows no more than one), the application’s icon, and .DirIcon, the icon of the AppImage itself. Where each of them lives and how tools find the icon is explained in How AppImage icons are stored and found.

The SquashFS superblock

The image begins with a 96-byte superblock. It holds the signature hsqs, the format version (4.0), the block size, the positions of the inode, directory and fragment tables, and one compression ID that applies to the whole image. A reader needs a decompressor for exactly that method, and not every AppImage reader has the same ones: AppImage compression: gzip, zstd and xz lists which reads what.

Type 1 and type 2

Type 1 AppImages, the older format, are ISO 9660 images that are also ELF executables, marked with 0x41 0x49 0x01 at offset 8. Type 2 replaced the ISO image with a file system appended to the ELF runtime. The specification only asks for “a filesystem that the ELF part can mount”; type2-runtime mounts SquashFS. Some other runtimes also support DwarFS.

Looking inside without running it

The documented ways to look inside a type 2 AppImage go through its runtime: --appimage-extract unpacks the image into squashfs-root, --appimage-mount mounts it, and --appimage-offset prints the offset for mount. The AppImage documentation notes that there is currently no way to extract without calling the AppImage, which is not always appropriate for a file you do not trust.

Because the offset follows from the ELF headers, the same work can be done on the file as data: read the ELF header and the last section header, open the superblock at the computed offset, and read tables and files from there. That is how UNQIRO reads an AppImage, in your browser, in small pieces, without executing anything.

What UNQIRO reads, and what it does not

  • Type 2 AppImages with a SquashFS 4.0 image compressed with gzip or zstd. It reads the ELF header, the last section header, the superblock, the tables it needs, the desktop file and the icon files, not the whole file.
  • Not read: images compressed with lzma, lzo, xz or lz4, which are named in the message; AppImages that store their files in DwarFS instead of SquashFS; type 1 AppImages.
  • Linux programs without the AI 0x02 marker are not AppImages. They usually keep their desktop icons outside the executable.

Check your own file

The AppImage tool reads your file in the browser as data. It shows the architecture, the file system and its compression, and lists the icons it finds. Nothing is uploaded, and nothing in the file is run.

Inspect an AppImage without running it (AppImage Icon Extractor)

Sources