Summary
- The framework's container manager and app installation parsers read text logs that the iOS 27.0 sysdiagnose we studied does not hold. On that archive both return nothing.
- The pull request adds a parser,
containermanager_state, for the JSON file that holds the same information:logs/ContainerManager/state.json. - It is the sibling of iLEAPP pull request #2509: both read the same file with almost the same reading code. This post explains how the two projects differ in structure.
- The pull request is open and in review. Its text reports 4,744 events from the research image, 20 new unit tests, and one test skipped for lack of iOS 27 test data.
- 4,744events from the research image
- 7record types in the file
- 20new unit tests
- 1,583lines added in 6 files
Cite as
Addai, D., SectraTEK Lab (2026). A container state parser for the sysdiagnose framework. EC-DIGIT-CSIRC/sysdiagnose pull request #262, in review. https://github.com/EC-DIGIT-CSIRC/sysdiagnose/pull/262
Methodology note
Drafted with AI assistance and reviewed by the author before publication.
The issue
The sysdiagnose analysis framework from EC-DIGIT-CSIRC turns a sysdiagnose into events that can be searched and put on a timeline. It has two parsers for what an app container looks like on a phone: containermanager reads the container manager's text log, and appinstallation reads the installer's logs. On the iOS 27.0 archive we studied, the directories those parsers read, logs/MobileContainerManager and logs/appinstallation, are not in the archive. Both parsers return nothing, and an examiner who sees an empty result may conclude that the phone held no app containers.
The records had moved to a JSON file, logs/ContainerManager/state.json, which no parser read. Figure 1 shows the two situations.
One caution about the cause. In the nine iOS 26 archives we hold, the two text-log directories were already missing, so our data cannot place the change at iOS 27. The state file, though, appears only in the iOS 27.0 archive.
The essence
The pull request adds one parser, containermanager_state, for iOS 27.0 and later. It reads the state file and emits one event per record: app data, app group, extension and system containers with their directory times, the extension-to-app map, the code-signing registration cache, the recent callers of the daemon, the class paths and the persona tables. The reading is done by a separate module, utils/container_state.py, which never raises, keeps unknown keys in a column named other, and rejects out-of-range times instead of inventing a date.
| Record type | What a record is | Events |
|---|---|---|
| container | An app data, app group, extension or system container, with directory times if it has a directory | 2,157 |
| codesign | An entry of the code-signing registration cache | 1,799 |
| child_parent | An extension identifier paired with its host app | 637 |
| managed_path | A managed path of an identity | 67 |
| caller | A recent caller of the container manager daemon | 58 |
| class_path | A container class and its directory | 15 |
| persona | A persona identity | 11 |
The documentation page that ships with the parser separates what the file shows from what is inferred. It shows a container's directory times and its stored generation. It does not show who installed an app: a container's birth time is about when the device created the container, and a record with no directory cannot be told apart as removed, replaced or not yet created.
How this relates to iLEAPP #2509. The two contributions read the same file, and the reading core was written once. Compared line by line, the two copies differ in three places, all from the iLEAPP review: how a numeric time is read, the state word of a registration with no flag set (blank in iLEAPP, "registered" here), and a Python import line. In iLEAPP the core sits in the artifact file and three report functions split it by whether a container has a directory (see the iLEAPP post). In the framework the core sits in a utility module, and the parser turns every record into an event. Figure 4 sets the two structures side by side. Each follows what its project expects.
Our methodology
The method is the one described in the iLEAPP post, applied to a second project. The file's layout was read on one iOS 27.0 archive and each statement tied to a measurement. A container record always has the same eight keys. Its directory name appears in the archive's installation log, while its own identifier does not. A null directory entry goes together with a stored generation of -1 in the records measured. Within the records that have a directory, the birth and modify times are not after the change time, which is not after the last-seen time, in all 1,738.
The parser then follows the framework's own conventions. It looks for the file at a fixed path in the case folder, is gated by ios_version to 27.0 and later, and declares jsonl as its output format (Figure 2). A record with a time becomes an event at that time. A record without one, such as a container with no directory, becomes an event at the sysdiagnose creation time with a description that says the record carries no timestamp, so it cannot be mistaken for a measured time.
Validation is a recount. The tests count the records of each type directly in the file, without the row builder, and compare. On the research image all seven per-type counts equal that independent count and add up to 4,744 events, with no warnings; running the parser twice gives identical output (Figure 3). The 20 new unit tests also cover damaged and truncated files, which return nothing without raising, bad and out-of-range times, very large numbers, unknown keys, and flags that override the state.
Show this figure as a table
| Record type | Events |
|---|---|
| Container records | 2,157 |
| Code-signing registrations | 1,799 |
| Extension-to-app pairs | 637 |
| Managed paths | 67 |
| Callers of the daemon | 58 |
| Class paths | 15 |
| Personas | 11 |
| Total | 4,744 |
Test data is a file generated by a script with invented values, so no case data is in the repository. One test is skipped, because the framework's test data repository holds no iOS 27 case. The pull request offers to contribute a small synthetic fixture there if the maintainers want the case-based test to run. The lint check is clean. Eight existing tests error with and without this branch, because the test data submodule was not checked out in the Lab's environment. The pull request records this rather than hiding it.
The value
For users of the framework the value is continuity: an iOS 27 sysdiagnose gives container events where the older parsers give none. The events can be searched with the rest of the sysdiagnose output, and extension containers carry the host app the file pairs with them. Examiners who already work in the framework do not need to leave it to read one file.
The comparison also shows something about method. Two projects with different structures can share one tested reading core, so a fix or a caution found in one place carries to the other. Figure 4 shows where the versions still differ. Numeric times are the point to watch. In the iLEAPP review, the maintainer asked that a numeric time not be turned into a date by guessing its unit, because a count of seconds in the Cocoa epoch falls inside the Unix range and was read as a date in 1994. The framework version as submitted converts a number by guessing the unit and marks it in the notes. In the research image every time is an ISO-8601 string, so this does not change any count. Whether the framework wants the same rule is for its maintainers.
Show this figure as a table
| Aspect | iLEAPP #2509 (merged) | SAF #262 (in review) |
|---|---|---|
| Code layout | One artifact file holds the reader and three report functions | Reader in utils/, a 93-line parser, and a docs page |
| Finding the file | By path pattern, in an archive, a folder or on its own | Fixed path in the case folder; iOS 27.0 and later only |
| Output | Three reports; times first in the first one | One stream of events, one per record |
| Record with no time | Blank time; not on the timeline | Dated at sysdiagnose creation time and described as such |
| Numeric time | Left blank, raw value in the notes | Converted with a guessed unit, marked in the notes (as submitted) |
| Tests | Registered cases with baselines plus unit tests | 20 unit tests; case-based test skipped |
The limits are those of the iLEAPP contribution. The events are evidence of what the container manager daemon knew at capture. A container with timestamps shows a directory existed; a record without a node shows none existed then. Neither shows use, installation or removal by a person. Only one iOS 27.0 archive was read, so the parser says nothing about other versions or devices.
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 (version 1.2, preprint, not peer reviewed; doi:10.5281/zenodo.23720069). Section 3.4 names this parser as the port of the container-state reader to the framework, and Section 4.5 explains why its container count (2,157) differs from the iLEAPP split (1,738 and 419). The report is on the Lab site at LAB-TR-0011.
LAB-TR-0010, on reading new sysdiagnose files, is in preparation and has no public page. 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.
| Number or claim | Where it came from |
|---|---|
| 4,744 events: 2,157 container, 1,799 code-signing, 637 extension-to-app, 58 caller records, 15 class-path, 11 persona, 67 managed-path; 0 warnings; deterministic | PR text, section Tested |
| 20 new unit tests; one skipped; lint clean; 8 existing tests error with and without the branch | PR text, section Tested; tests in the diff |
| 1,583 lines added in 6 files | GitHub pull request metadata (additions, files) |
| Parser gated to iOS 27.0 and later, jsonl output, 93-line parser file | src/sysdiagnose/parsers/containermanager_state.py in the diff |
| Records with no time dated at sysdiagnose creation time | Parser file in the diff |
| 26 columns, 8 record types, unknown keys in other | utils/container_state.py in the diff |
| Numeric times: unit guessed and marked in the notes (as submitted) | parse_timestamp in utils/container_state.py in the diff |
| iLEAPP review: numeric time left blank; Cocoa-epoch number read as 1994 | iLEAPP #2509 review thread, maintainer comment |
| Eight keys per container record; directory name in installation log; generation -1 with a null entry; time ordering in all 1,738 records | docs/containermanager_state.md in the diff |
| Open; no reviews or comments; branch updated with main on 29 October 2026; opened 18 October 2026 | GitHub pull request metadata (state, reviews, comments, commits, createdAt) |
| Text-log directories already absent in nine iOS 26 archives | LAB-TR-0011 section 4.2 |
| Container count differs from the iLEAPP split by construction | LAB-TR-0011 section 4.5 |
| The two reading cores differ in three places (numeric time, registration state word, an import line) | Line-by-line comparison of the core in the SAF diff and in the iLEAPP #2509 diff |
| Offer of a small synthetic fixture for the test data repository | PR text, section Tested |