Open Source / iLEAPP #2509

Reading app containers from the container manager state

A recent iPhone sysdiagnose can open cleanly and still give the container report nothing: the text logs the existing reader depends on are not in the archive, and the same information now sits in a JSON file the report never touched. This contribution reads that file and turns it into three reports.

  • Daniel Addai, SectraTEK Lab
  • Merged
  • CC BY 4.0

View the pull request on GitHub (opens in a new tab)

Summary

  • In the iOS 27.0 sysdiagnose we analysed, the container manager's text logs are not in the archive. The same information sits in a JSON file, logs/ContainerManager/state.json, about 4 MB.
  • The existing container artifact reads the text logs, so on that archive it returned nothing. The pull request adds three artifacts that read the JSON file: containers with a directory, containers without one, and the code-signing registrations.
  • On the Lab's research image the reports hold 1,738, 419 and 1,799 rows, and each count equals a separate count of the file's records.
  • A row is a lead, not proof. A container's birth time is not an install time, and a container with no directory cannot be called removed.
  • 3artifacts added
  • 1,738containers with a directory
  • 419containers without a directory
  • 1,799code-signing registrations

Cite as

Addai, D., SectraTEK Lab (2026). Container manager state in the iOS sysdiagnose: an iLEAPP contribution. iLEAPP pull request #2509, merged 19 October 2026. https://github.com/abrignoni/iLEAPP/pull/2509

Methodology note

Drafted with AI assistance and reviewed by the author before publication.

The issue

An examiner is asked whether an app was ever on a phone, and the only evidence is a sysdiagnose from a recent iPhone. In iLEAPP the container artifact, mobileContainerManager, answers that kind of question from the container manager's text logs. On the iOS 27.0 archive we studied, the two places it looks, logs/MobileContainerManager and logs/appinstallation, are not there. The report comes back empty and raises no error. An empty report reads as "nothing found", which is the wrong thing to conclude.

The records were not gone. They had moved. The archive carries logs/ContainerManager/state.json, a dump of the container manager daemon's state, and no artifact read it. Figure 1 shows the two layouts side by side.

Where the container information sits in the iOS 27.0 archiveTwo text-log paths are absent from the archive and the existing artifact returns nothing. The state.json file is present and is read by three new artifacts.Inside the iOS 27.0 archive we studiedlogs/MobileContainerManager/*Text log read by the existing artifactNot in the archivelogs/appinstallation/*Text log read by the existing artifactNot in the archivelogs/ContainerManager/state.jsonJSON file, about 4 MBPresent; no artifact read itBefore: the existing container artifactIt looks for the two text logs, finds neither,and returns no rows and no error. An empty report reads as nothing found.After: three artifactsRead the JSON file directly:1,738 + 419 + 1,799 rowsOur collection cannot place the change at iOS 27: the two text-log directories were already absentin nine iOS 26 archives (LAB-TR-0011, section 4.2). The state file itself is new relative to all nine.
Show this figure as a table
Paths and what reads them
Path in the archiveIn the iOS 27.0 archiveRead by
logs/MobileContainerManager/*AbsentExisting container artifact (returns nothing)
logs/appinstallation/*AbsentExisting container artifact (returns nothing)
logs/ContainerManager/state.jsonPresent, about 4 MBThree new artifacts
Figure 1The existing artifact depends on text logs that the iOS 27.0 archive does not hold; the same records sit in a JSON file that is now read. Source: pull request text; LAB-TR-0011 section 4.2 (diagram, row counts from the research image).

Two cautions apply. First, the state file is not a drop-in replacement for the text logs: it is a snapshot of what the daemon holds at capture, with directory times, and it has no installer messages. Second, our wider comparison cannot say when the text logs disappeared. The two directories were already absent in the nine iOS 26 archives we hold, so the pull request's wording that iOS 27 archives "no longer carry" them is true of the reader's inputs, but our data cannot place the change at iOS 27. The state file is the part that is new.

The essence

The contribution reads the state file and produces three reports. The file has one section per scope: the system scope and one user. Each container is a record with an identifier, a directory name, a stored generation number and a directory entry. That entry either holds four times (birth, change, modify and last seen) or is null. The first report lists the records with a directory, with the times first so the rows reach the timeline. The second lists the records whose entry is null. The third lists the code-signing registration cache.

The three artifacts
ArtifactA row isMain columnsRows on the research image
Container Manager StateA container record that has a directory entryBirth, change, modify and last-seen time (UTC); container class; identifier; host app for extensions; state; path; generation; scope1,738
Container Manager State - No DirectoryA container record whose directory entry is nullContainer class; identifier; directory name; generation; scope; notes419
Container Manager State - RegistrationsOne entry of the daemon's ordered code-signing cacheIdentifier; container class; flags set; number of entitlement keys; position in cache1,799

The notes shipped with each artifact say what the file does not establish. A container with no directory could have been removed, replaced or not yet created, and the file cannot tell these apart. A registration is an identifier the daemon registered; on the research image these included extension and group identifiers, so it is not a list of installed apps. Entitlement values are not copied, only how many keys there are.

The maintainer's review changed two things, both in the direction of caution. A numeric time value is no longer turned into a date by guessing its unit, because a count of seconds in the Cocoa epoch also falls inside the Unix range and came out as a date in 1994; the cell stays blank and the raw value goes in the notes. Labels and notes were also reduced to what the file and the code show.

Our methodology

We started from the file, not from a specification. The layout was read on one iOS 27.0 archive and every statement was tied to a measurement. The directory name of a container (pathIdentifier) appears in the archive's installation log; the container's own uuid does not, and differs from the directory name. Of the 419 records with a null entry, 418 carry a generation of -1. Within the records that have a directory, the stored times never run backwards: the birth and modify times are not after the change time, which is not after the last-seen time (Figure 4).

The reader is written so that one damaged value costs one cell. It returns no rows for an empty or truncated file, rejects times outside a sane range instead of inventing a date, keeps unknown keys as text, and reports problems through the log and the notes column rather than raising. Figure 2 shows the path from file to reports.

From state.json to three reportsThe file passes through four steps: load, walk each scope, split each container record, read the directory entry. The result is split into three reports.state.jsonTwo scopes: <system>and one user idContainers, caches,class and persona tables1 Load the JSONEmpty or damaged file: no rows2 Walk each scopeSystem scope, then the user id3 Split each container recordGroup name gives class and persona4 Read the directory entryNull: no directory. Else four timesContainer Manager StateRecords with a directory, times firstTimeline, HTML, TSV, LAVANo DirectoryRecords whose entry is null, no timesHTML, TSV, LAVARegistrationsCode-signing cache, in file orderHTML, TSV, LAVA
Figure 2The reader never raises on a bad file, and the directory entry decides which of the first two reports a container record lands in. Source: scripts/artifacts/containerManagerState.py in the pull request (diagram).

Validation had three layers. First, an independent recount: a function that counts the file's records without using the row builder, compared with the report rows for each artifact. Figure 3 shows the result on the research image: the counts agree for the HTML, TSV and LAVA outputs and the timeline. Second, the same rows come out of a .tar.gz, an extracted folder and a folder holding the archive. Third, the repository's own checks for claim language, descriptions, paths and HTML safety pass, and the lint score on the changed files is 10.00 of 10.

Rows in each report against records counted in the fileThree pairs of bars of equal length: 1,738 containers with a directory, 419 without and 1,799 registrations, each counted in the file and in the report.05001,0001,5002,000With a directory1,738 in the file1,738 in the reportNo directory419 in the file419 in the reportRegistrations1,799 in the file1,799 in the reportRecords (count)
Show this figure as a table
Records in the file and rows in the report
ReportRecords counted in the fileRows in the report
Container Manager State1,7381,738
Container Manager State - No Directory419419
Container Manager State - Registrations1,7991,799
Figure 3For each report the row count equals a separate count of the records in the file, on the research image. Source: pull request text (Tested section): counts from the iOS 27.0 research image; real counts, not synthetic.

Test data followed the project's rule that fixtures carry no case data. The first fixture is generated by a script with invented identifiers and covers the edge cases: numeric times, damaged input and unknown keys. During review the maintainer asked for a fixture with the real file's structure. We committed a copy of the research image's file with every identifying value replaced (bundle identifiers, team identifiers, UUIDs and host names) and all times shifted by one constant offset; nothing else in the file was changed. Both fixtures are registered test cases with recorded baselines. The first baselines used Windows path separators and failed on the project's Linux runner, so they were re-recorded; baselines recorded on Linux or macOS avoid the problem.

The value

The people who benefit are examiners and researchers who receive an iOS 27 sysdiagnose and want to know which containers the device knew about. The artifact answers a narrow question: which app data, app group, extension and system containers existed at capture, and when did each directory come into being? Extension containers carry the host app that the file pairs with them, so an extension can be tied to its app. The birth times go onto the timeline with every other source.

In a case this is a corroborating source, not a finding. A birth time near an event of interest is an indicator that the container was created then; it does not show who installed the app or that anyone opened it, and it can reflect a restore or a re-creation. Figure 4 sets out what the stored times do and do not carry.

Order of the four stored times and what they do not showbirthtime and mtime are not after ctime, and ctime is not after lastSeen, in all 1,738 records with a directory. The times do not show who installed or opened an app.Four times stored for one container directory (synthetic example)birthtimeStored birth timemtimeStored modify timectimeStored change timelastSeenStored; cause not establishedAn arrow: the earlier time is not after the later oneThese times do not showwho installed the app, when anyone opened it, or whether the container was later removed.A birth time is about when the device created the container, not when a person installed an app.
Figure 4The ordering held in every record with a directory on the research image; its meaning is read cautiously. Source: docs page of the sibling pull request (SAF #262), measured on the same file; schematic with synthetic labels.

The limits are as important as the rows. The 419 records without a directory are leads about containers that once had, or will have, a directory; they are not removals. An app without a row is not shown to be absent, because the file is a snapshot and records can be missing for reasons outside it. Only one iOS 27.0 archive was used, so the artifact says nothing about other versions, and it has not been run on more than one iOS 27 device.

Where this sits in the Lab's research

This contribution supports LAB-TR-0011, Log Compression and Archive Structure in the iOS 27.0 Sysdiagnose: An Early Observation Tested Against Ten Archives (Lab Technical Report, version 1.2, preprint, not peer reviewed; doi:10.5281/zenodo.23720069). Section 4.2 shows the state file is present only in the iOS 27.0 archive among ten, and Table 6 lists the row counts of this reader. The report is on the Lab site at LAB-TR-0011.

LAB-TR-0010, which describes the reading rules for these new files, is in preparation and has no public page yet. Until it appears, the method above rests on the pull request.

Sources

Every number and claim above, with where it came from. PR means the pull request text, its diff or its review thread.

Sources for the numbers and claims
Number or claimWhere it came from
Three artifacts added; containers with a directory 1,738; without 419; registrations 1,799PR text, section Tested; artifact notes in scripts/artifacts/containerManagerState.py
Counts equal a separate count of the file's records; HTML, TSV, LAVA and timeline matchPR text, section Tested; function expected_counts in the diff; Figure 3
state.json about 4 MB; the two text-log directories absentPR text, first paragraph
Text-log directories already absent in nine iOS 26 archives; state file new relative to all nineLAB-TR-0011 section 4.2
418 of 419 no-directory records carry generation -1Artifact notes in the diff (containerManagerStateNoDirectory)
Birth and modify times not after change time, which is not after last-seen time, in all 1,738 recordsdocs/containermanager_state.md in the sibling pull request, SAF #262
Maintainer review: numeric times left blank (a Cocoa-epoch number came out as a date in 1994), labels trimmed, time columns firstPR review thread, maintainer comment
Real-structure fixture with identifying values replaced and times shifted by one offset; baselines re-recorded for the Linux runnerPR review thread
Lint 10.00 of 10; repository checks passPR text, section Tested
Merged 19 October 2026GitHub pull request metadata (mergedAt)
Reader rows on the iOS 27.0 archive in the paperLAB-TR-0011 Table 6