Summary
Two reset-related stores sat in places ALEAPP did not read well. The boot reason history lives in a binary property store that a text scan cannot walk, and the setup flags are spread over a settings file per user, two history files and a few properties. This change decodes the first and gathers the second.
It extends Persistent Properties - Matched Text Observations so that, when the file is a property store, each boot reason entry becomes a row with its reason, subreason and stored time. It also adds a new artifact, Setup Flags, in the Wipe & Setup category. Values are shown as stored and no row is judged: a boot reason entry is a text the platform kept, not an indicator of a wipe by itself.
- 2,416lines added, 42 removed
- 32files changed
- 11synthetic case zips
- 27 and 31Setup Flags rows on two images
The issue
An examiner asks a short question about a phone: when did it last restart, and what did the platform record as the reason? Android keeps a boot reason history in a property called persist.sys.boot.reason.history, one line for each boot, in the form reason, optional subreason, and the time in seconds. On the tested Android 16 phones the property sits in /data/property/persistent_properties, and that file is not text. It is a protobuf store: a repeated record whose first field is the property name and whose second field is the value.
The artifact that looked for these entries scanned the file as text. On the two Samsung images tested it reported no row at all. The file held entries; the reader could not walk the records to reach them. Across the 14 file system extractions in the Lab’s reset study the text scan found 3 of the 42 history entries that reading the records gives. Nothing in the output hinted at the loss: an empty table looks the same as a phone with nothing to report.
Show this figure as a table
| Image | Before | After | Independent count |
|---|---|---|---|
| Samsung S-series, file system zip | 0 | 2 | 2, separate protobuf walk |
| Samsung A-series, file system zip | 0 | 3 | 3 |
android_boot_reason.py (synthetic example).The second gap is about setup. Whether a phone finished its first-run setup is recorded in flags such as user_setup_complete and device_provisioned. On the tested images these sit in the settings file of each user, in the binary ABX form, with a change history in JSON files and a few matching properties in the same store as the boot reasons. An examiner who wanted to see when those flags changed, and which package wrote them, had to open four files in two formats. Because the flags also go from 1 to 0 and back to 1 around update entries, reading one value without its history can mislead.
The essence
When the file is a persistent property store, it is decoded directly, with no protobuf library: one row for each line of the boot reason history and one for a non-empty persist.sys.boot.reason. Any other file, or a store without boot reason entries, is read with the earlier text rules, unchanged, and the added columns stay blank. Setup Flags then reads the current setup settings of each user, the history entries of those settings, and the setup properties, and puts them in one table.
Times are read as UTC only inside a stated range: seconds for a boot entry, milliseconds for a settings history entry, in both cases only between 2009 and 2100. Any other value is shown as stored, with no UTC. A boot reason entry that contains the word factory_reset is marked, in a yes or no column that says only that the word appears.
| Item | What it reads or adds | Status |
|---|---|---|
| Persistent Properties - Matched Text Observations | persistent_properties as a protobuf store: persist.sys.boot.reason.history (one row per line) and persist.sys.boot.reason | Extended |
| Appended columns | Property, Reason, Subreason, Position, factory_reset Reason Present | 5 columns |
| Setup Flags (Wipe & Setup) | settings_secure.xml of each user; settings_history_secure.json, settings_history_global.json; setup properties of the store | New artifact |
| Names reported | 11 settings (for example user_setup_complete, device_provisioned, setup_skipped) and 3 properties | Listed in the notes |
| Not read | settings_global.xml: its current values are rows of Settings Global | By design |
Our methodology
Format discovery. The store was read as bytes first. The layout is a repeated record, with the property name as field 1 and the value as field 2, both strings; the reader walks the varints itself, caps the number of records at 200,000 and each value at 1 MiB, and treats a file whose first bytes are not a record as not a store, so the text rules still apply. A file cut inside a record gives the records before the cut and a log line. Position is 0 for the first line stored; because the platform inserts each new entry at the front, 0 is the entry written last.
The settings side was handled the same way. The settings files are ABX on the tested images, which the tool’s existing reader decodes, and plain XML on four of the 19 corpus keys. The history files are JSON objects whose keys are setting names and whose values are lists of entries with a mode, an old and a new value, a package and a time. The first entry of each setting holds a small number as its time on the tested images; it is shown as stored, with no UTC, and what it means was not examined.
android_boot_reason.py, android_setup_flags.py, persistentProp.py and androidSetupFlags.py in the diff (diagram).Synthetic fixtures. The pull request adds a fixture builder, a unit test file and eleven case zips with baselines, all with invented values. Cases constructed only include a factory_reset entry, entries without an epoch, text, small and huge epochs, CRLF inside a value, a cut store, a text file in place of the store, plain XML settings, a second user with a different value, cut ABX, XML and JSON files, and JSON that is not an object. A copy under a mirror folder is skipped, as Settings Global does.
Independent recount. The row counts were compared with a separate protobuf walk and a separate parse of the settings and history files, made without the tool (Figure 3). The six current global values of each image, which Setup Flags does not read, were checked against the Settings Global rows and agreed in name and value. Both settings files on the tested images were in the ABX form, so the plain XML path is covered by fixtures and by other corpus keys.
Show this figure as a table
| Image | Current values | History entries | Properties | Total |
|---|---|---|---|---|
| Samsung S-series, file system zip | 4 | 20 | 3 | 27 |
| Samsung A-series, file system zip | 5 | 23 | 3 | 31 |
androidSetupFlags.py. Aggregate counts only, no synthetic values.Missing data. Bug reports were compared too. Their boot reasons and setup values are already rows of other bug report artifacts, so none was added. The pull request lists what it did not examine: Android before 16 with the store reading, vendors other than Samsung, a store with another record layout, and the system settings namespace, where no setup name was found. The empty value of persist.sys.boot.reason on both images gives no row, and the fixture is the only source of a factory_reset entry.
The value
The boot reason rows give an examiner a short, readable restart trail with the reason the platform kept for each boot. Two limits come with it, and both are stated in the artifact notes. The history keeps a handful of entries, so an older reset is not expected in it. In the Lab’s study it held three entries on twelve of fourteen extractions, two on one and four on one, and none of the 65 entries on extractions and bug reports was a factory-reset entry. On the two phones that could be checked, the oldest boot entry was 104 and 598 days after the last wipe request, so the boot that followed the wipe had rolled off. A stored entry that does not contain the word therefore says nothing about earlier history.
The setup table answers a different question: what state the setup flags are in now and what their history shows, with the writing package beside each value. Seen with the recovery history of the companion change, it supports a careful reading. Of 13 flag cycles the Lab counted on 8 devices, none of those devices held a wipe entry, and five lay within about 15 minutes of an update entry. A flag cycle is therefore weak evidence by itself, and a boot reason entry is a text the platform kept. Together with a recovery entry that carries the wipe option, they are supporting indicators.
androidSetupFlags.py and LAB-TR-0004, Section 5.6 (synthetic example).Time needs the same care as in the recovery history. The epoch of a boot entry is the device clock when the platform wrote it; on one of the 19 corpus keys the epoch of the newest entry was older than the two after it. Any date taken from these fields is an estimate that rests on a recorded time and should be checked against another clock before it is reported.
Where this sits in the Lab's research
The pull request is the reader side of the Lab’s study of reset indicators: Addai, D. (2026). Informative but Individually Unreliable: An Empirical Study of Android Reset Indicators and Their Clocks. Lab Technical Report LAB-TR-0004, version 1.1, Zenodo, doi:10.5281/zenodo.23720065 (all versions: 10.5281/zenodo.23717926). Read the report on this site. Sections 5.5 and 5.6 hold the boot reason and setup flag measurements used above, and Section 5.7 the comparison in which the text scan found 3 of 42 entries.
Related Lab Technical Reports, all available on this site as preprints with the Zenodo deposit pending: LAB-TR-0028 lists the setup flags among the traces of the start of a phone’s own life and notes that they mark a state with no time; LAB-TR-0031 counts boot reason entries among the traces of a restart in its footprint classes of acquisition, with 2 to 4 entries on every extraction (median 3), which is why a recent entry can follow a restart linked to the acquisition itself; and LAB-TR-0040 shows, on a stated model, that a restart and a boot reason share a cause, so counting them as separate classes understates the chance of a false alarm. LAB-TR-0041 sets out when a gap in an event history may be read as an absence. No Lab report covers the Setup Flags artifact itself; for it the method above is from the pull request.
Sources
Every number and claim above, with where it comes from.
| Number or claim | Source |
|---|---|
| Pull request merged 28 October 2026; 2,416 additions, 42 deletions, 32 files; 11 case zips | Pull request metadata (gh pr view) |
| Boot reason rows 2 and 3, 0 before; independent count 2 and 3 | Pull request body, table "Proven on real sources" |
| Setup Flags rows 27 (4 + 20 + 3) and 31 (5 + 23 + 3); 6 current global values equal | Same table; notes of androidSetupFlags.py in the diff |
| Record cap 200,000; value cap 1 MiB; epoch range 2009 to 2100; 11 settings and 3 properties | MAX_RECORDS, MAX_VALUE_BYTES, EPOCH_FLOOR, SETUP_SETTINGS, SETUP_PROPERTIES in the diff |
| Position 0 is the entry written last; at most 4 history entries; 2 to 4 in the files tested | Notes of persistentProp.py in the diff |
| 3 to 12 rows on each of 19 corpus keys, Android 10 to 17 | Notes of androidSetupFlags.py in the diff |
| Eleven passes; setup flags 1, 0, 1 around update entries on two sources | Pull request body, "Test record" and "Setup Flags: what it reads" |
| 42 history entries on 14 extractions (three on twelve, two on one, four on one); 23 on 7 bug reports; none of 65 a factory-reset entry | LAB-TR-0004, Section 5.5 |
| Text scan found 3 of 42 entries | LAB-TR-0004, Table 9 |
| Oldest boot entry 104 and 598 days after the last wipe request on two phones | LAB-TR-0004, Section 5.5 |
| 13 flag cycles on 8 devices; 5 within about 15 minutes of an update entry; 4 by 606 to 914 s | LAB-TR-0004, Table 3 and Section 5.6 |
| Setup flags mark state only, with no time; 2 to 4 boot entries, median 3; shared cause 0.214 against 0.084 | LAB-TR-0028, Table 2; LAB-TR-0031, Section 6; LAB-TR-0040, Section 4.2 and Table 4 |
| Figures 1, 2 and 4 are diagrams with synthetic values; Figure 3 uses the counts above | Author |