Open Source / ALEAPP #1835

Boot reasons and setup flags, decoded

The boot reason history of an Android phone sits in a binary property store that a text scan cannot walk, and its setup flags are spread over several files. This contribution decodes the first and gathers the second.

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

View the pull request on GitHub

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.

The property store layout and rows reported before and afterLeft: a protobuf property store holding repeated records; one record has the property name as field 1 and a value of three boot reason lines as field 2, with synthetic epochs. Right: rows reported on two images, 0 before and 2 and 3 after.What the file holds (synthetic example)Rows reported on two imagespersistent_properties: a protobuf file written by initone record, repeated for every propertyfield 1namepersist.sys.boot.reason.historyfield 2value, one linefor each bootreboot,userrequested,1767000000shutdown,userrequested,1766900000reboot,ota,1766800000other records: other property names and valuesnot boot reason entries0123Samsung S-series0 rows before2 rows afterSamsung A-series0 rows before3 rows after
Show this figure as a table
Rows reported before and after
ImageBeforeAfterIndependent count
Samsung S-series, file system zip022, separate protobuf walk
Samsung A-series, file system zip033
Figure 1Left, the layout of the property store with invented values. Right, the number of boot reason rows the artifact reported on two images before and after reading the store as records. Source: pull request body, "persistentProp: what it reads" and the table "Proven on real sources"; layout from 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.

Artifacts and columns in this change
ItemWhat it reads or addsStatus
Persistent Properties - Matched Text Observationspersistent_properties as a protobuf store: persist.sys.boot.reason.history (one row per line) and persist.sys.boot.reasonExtended
Appended columnsProperty, Reason, Subreason, Position, factory_reset Reason Present5 columns
Setup Flags (Wipe & Setup)settings_secure.xml of each user; settings_history_secure.json, settings_history_global.json; setup properties of the storeNew artifact
Names reported11 settings (for example user_setup_complete, device_provisioned, setup_skipped) and 3 propertiesListed in the notes
Not readsettings_global.xml: its current values are rows of Settings GlobalBy 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.

Two pipelines from stored files to report columnsLane A: persistent_properties bytes, field walk, boot reason properties, entry split, epoch read, then six columns. Lane B: settings_secure.xml, settings history JSON and setup properties as three inputs, a filter on setup names, a time read, then the Setup Flags columns.A Persistent Properties - Matched Text ObservationsProperty storepersistent_propertiesWalk the fieldsvarints, no libraryKeep boot reasonhistory, single valueSplit each entryreason, subreasonRead the epochUTC if 2009 to 2100Property, Reason, Subreason, Position, factory_reset Reason Present, Timestamp; raw text keptB Setup Flags: three inputs, then two stepssettings_secure.xmleach user, XML or ABXSettings historysecure and global JSONSetup propertiespersistent storeKeep setup namessettings, propertiesRead the timeUTC if ms 2009 to 2100Namespace, User, Record, Value, Written By, Old Value, Time (UTC), Time As Stored, LocationNames and columns come from the diff; the boxes are a diagram, not code.
Figure 2Lane A turns the property store into boot reason rows. Lane B gathers the setup flags from three kinds of file into one table. A time is read as UTC only inside a stated range; otherwise it is shown as stored. Source: 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.

Setup Flags rows by kind on two imagesTwo stacked horizontal bars. Image one: 27 rows, made of 4 current values, 20 history entries and 3 properties. Image two: 31 rows, made of 5 current values, 23 history entries and 3 properties. The counts equal an independent parse. Scale 0 to 35 rows.Current values (settings file)History entries (settings history)Properties05101520253035Samsung S-series, file system420327 rowsSamsung A-series, file system523331 rowsIndependent parse of the files: 4 + 20 + 3 and 5 + 23 + 3. The 6 current global values of each image,not read by this artifact, were equal in name and value to the rows of Settings Global.
Show this figure as a table
Setup Flags rows by kind
ImageCurrent valuesHistory entriesPropertiesTotal
Samsung S-series, file system zip420327
Samsung A-series, file system zip523331
Figure 3Setup Flags rows by kind on two images, one scale. Each total equals the count made by an independent parse of the same files. Source: pull request body, table "Proven on real sources (counts only)" and notes of 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.

A flag cycle next to an update entry on an invented timelineAn invented timeline in minutes. A recovery history entry for an update package at minute 0. The device_provisioned value falls from 1 to 0 at minute 12 and returns to 1 at minute 37. The note says a flag change is not by itself a reset indicator.Invented times (synthetic example)start+10 min+20 min+30 min+40 minRecovery historyupdate entryupdate package entrydevice_provisionedin the settings history101 to 0 at +12 minback to 1Seen in the tested sourcesFlags went 1, 0, 1 around update entrieson two sources.What the notes sayA change of these flags is not by itselfa reset indicator.
Figure 4A schematic cycle of the setup flag next to an update entry. The offsets are invented; the pattern, flags that go to 0 and return to 1 near an update entry, is the one the artifact notes describe. Source: pattern from the notes of 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.

Sources
Number or claimSource
Pull request merged 28 October 2026; 2,416 additions, 42 deletions, 32 files; 11 case zipsPull request metadata (gh pr view)
Boot reason rows 2 and 3, 0 before; independent count 2 and 3Pull request body, table "Proven on real sources"
Setup Flags rows 27 (4 + 20 + 3) and 31 (5 + 23 + 3); 6 current global values equalSame 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 propertiesMAX_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 testedNotes of persistentProp.py in the diff
3 to 12 rows on each of 19 corpus keys, Android 10 to 17Notes of androidSetupFlags.py in the diff
Eleven passes; setup flags 1, 0, 1 around update entries on two sourcesPull 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 entryLAB-TR-0004, Section 5.5
Text scan found 3 of 42 entriesLAB-TR-0004, Table 9
Oldest boot entry 104 and 598 days after the last wipe request on two phonesLAB-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 sLAB-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.084LAB-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 aboveAuthor