Open Source / ALEAPP #1782

Reading every Android user's database

When a phone has a second Android user, an artifact that takes the first database it finds can report the wrong user's data. This contribution reads one database for each user in four artifacts and names the user in the report.

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

View the pull request on GitHub

Summary

Four ALEAPP artifacts took the first database they found and ignored the rest. On a Samsung phone with a Secure Folder, the second user’s small copy of the database can sort first, and then the report shows that copy as if it were the whole device. This change makes the artifacts for the Play Store app-update database, Google Calendar, Settings Search and My Files recents read one database for each Android user and add an Android User column.

The effect is measured on four research images: on three of them the earlier output held only the second user’s rows. A helper was added to the shared storage module so other artifacts can read the same way.

  • 4artifacts changed
  • 795lines added, 103 removed
  • 6synthetic test cases
  • 1,793primary-user rows a first-match read missed

The issue

An examiner opens the app-update report for a Samsung phone and finds 376 rows. The phone looks lightly used. In fact the primary user’s own database holds 494 rows, and the phone also has a Secure Folder with a smaller database of 376. The artifact did not misread a row. It read the first database whose path matched, and the Secure Folder user’s folder, user 150, listed ahead of the primary user’s.

Because the output has rows, nothing looks wrong. On the four images the same pattern hid more: on three of them the earlier calendar artifact returned no events and the settings search artifact returned no rows, because the user 150 copy was the one read. The app-update artifact returned 376, 458 and 373 rows where the two users together held 870, 1,198 and 932. Across the three two-user devices that is 1,207 of 3,000 rows returned, all of them the second user’s, and none of the primary user’s 1,793 rows.

Taking the first database versus reading one per userThree paths in the order the tool lists them: a Secure Folder user database with 1 row, the primary user database with 3 rows, and a second view of the same primary database. Taking the first match returns 1 row. Reading one database per user returns 4 rows.Files found, in the order listed (synthetic example)What the report holdsdata/user/150/com.android.vending/databases/frosting.dbSecure Folder user: 1 row, listed firstdata/data/com.android.vending/databases/frosting.dbuser 0, the primary user: 3 rowsdata/user/0/com.android.vending/databases/frosting.dbthe same file as above, a second view of itTaking the first matchreads 1 database, returns 1 rowthe 3 rows of user 0 are not in the reportOne database per userreads 2 databases; the second view is collapsedreturns 4 rowsAndroid User column: 150, 0, 0, 0The counts are those of the unit test fixture, with invented package names.
Figure 1The failure in miniature. The small database of the second user sorts first, and an artifact that takes the first match reports it as the whole answer. Source: admin/test/scripts/test_multi_user_first_match.py in the diff and pull request body (synthetic example).

An examiner who relied on that report could state which apps were installed, or whether a calendar held any event, from the wrong user. The error does not show as a failure; it shows as a plausible answer to a narrower question. This kind of partial loss lies outside the outcome classes that a run summary reports, which is why it needs its own test.

The essence

Each of the four artifacts now reads every database that matches, once for each Android user, and appends an Android User column last. The user is named from the folder the database sits in. A second view of one file, such as the app folder reached through two paths, is collapsed so a database is not read twice, while a second user’s copy is kept.

The shared helper user_files() returns each path with its user and skips sidecar files such as the write-ahead log, because the name is compared in full. The helper android_user() reads the user from the path. Both live in storagePathViews.py, so other artifacts can use them.

Artifacts changed
ArtifactDatabase read once for each Android userAdded
App updates (frosting)Play Store frosting.dbAndroid User
Google Calendar (events and calendars)Calendar provider databaseAndroid User
Settings SearchSaved queries databaseAndroid User
My Files recentsrecent_files databaseAndroid User
Shared helperandroid_user() and user_files()In storagePathViews.py

Our methodology

How the loss was measured. The output of each artifact on the four Samsung images was compared with a count of the rows in each user’s database. The app-update artifact returned the second user’s count and none of the first. The finding came from a recount, not from an error, which is the point of the method: the artifacts produced rows, so no outcome class flagged them. The Lab then counted how many artifact modules share the first-match pattern, 67 of 529 module files in its sweep, which is a bound on scale and not a count of defects.

The change. The pull request touches four artifacts and one helper, with the Android User column appended last so earlier columns keep their position. It notes that anything downstream keyed on column position needs a look. The empty and constant columns the output checker flagged, such as Calendar Location and the My Files Source and Description, are named in the notes.

From files found to rows with an Android User columnFive steps: files found, collapse the duplicate views of one file, keep files with the exact name, name the Android user from the path, read each database and add the Android User column. Below, six invented paths and the user number each gives.Files foundevery path matchedCollapse viewsone file, one entryExact namesidecars are skippedName the userfrom the pathRead each oneappend Android UserStep 4 on invented pathsdata/data/p/databases/a.dbuser 0data/user/150/p/databases/a.dbuser 150data/user_de/10/p/databases/a.dbuser 10userdata/user/7/p/databases/a.dbuser 7data_mirror/data_ce/null/11/p/a.dbuser 11sdcard/DCIM/a.dbno user: outside app data
Figure 2The helper added to the shared storage module. It keeps each Android user once and names the user from the folder the database sits in. Source: scripts/artifacts/storagePathViews.py (user_files, android_user) in the diff; paths are invented (synthetic example).

The doubling test. A tree was built from real files in which the primary user’s databases also appear at a second user’s path and at a decoy package path. If the artifact reads one database per user, the row count must exactly double; if it takes the first match, it stays the same (Figure 3). All six outputs of the four artifacts doubled exactly. The same idea, as a storage-view mode of the repository’s test runner, ran on six new synthetic cases with invented values: they pass 12 of 12 checks, and the same cases fail all six second-user legs without the change. An automated check on the pull request had asked for fixtures, and these cases are the answer.

Independent recount. The numbers were recounted with count(*) on each user’s database, with no ALEAPP code. The recount caught an error of our own: an earlier text of the pull request gave 677 rows for My Files recents, a figure found on none of the four images. It was replaced by the counts in Figure 4.

The doubling test on a constructed treeA tree with four copies of one database: the original of user 0, a second view of it, a second Android user, and a decoy package. They contribute N, 0, N and 0 rows. The expected total is 2N, so an artifact that takes the first match reports N.One database placed four ways (invented paths)Addsdata/data/p/databases/a.dbthe original, user 0Ndata/user/0/p/databases/a.dba second view of the same file0data/user/10/p/databases/a.dba second Android userNdata/data/decoy/databases/a.dbanother package0Expected total: 2N rows. An artifact that takes the first match reports N.A drop in the second user shows as a count that does not double.
Figure 3The doubling test: a second user's copy must double the row count, while a second view of the same file and a decoy package must not change it. Source: pull request body, "Checked against a second app data directory", and LAB-TR-0006, Section 3.5 (synthetic example).

Tests and limits. The test record lists a lint score of 10.00 of 10, 138 passing cases (132 before plus the 6 new ones), 175 passes in the storage-view mode with the same three known failures as before, and six new unit tests that pass. The unit suite shows the same failures as the base on the Windows host that ran it. The pull request says plainly what it did not do: other modules that call the first-match function may share the pattern, and 24 artifact files do; they were not audited.

The value

For an examiner the Android User column answers a question that was easy to skip: whose data is this? A report that lists user 0 and user 150 rows side by side shows that a second user exists and how large its database is. It also changes totals. The app-update artifact returns 606, 870, 1,198 and 932 rows on the four images, and calendar events return 111, 3, 291 and 27 where the earlier output held none on three images.

It also changes how a missing result is read. Before the change, an empty calendar on a phone with a Secure Folder could mean no events or a second user sorted first. Now each user’s database is read and the column names it, so an empty result for one user is separate from a populated result for the other.

App-update rows by Android user on four imagesFour stacked bars on a scale from 0 to 1,200. Totals 606, 870, 1,198 and 932. User 0 holds 606, 494, 740 and 559; user 150 holds none, 376, 458 and 373. Before the change the artifact returned only the user 150 rows on three images.User 0 (primary user)User 150 (Secure Folder)02004006008001,0001,200Image 1 (S-series)606not affected: no second userImage 2 (A-series)870before: 376 rows, user 150 onlyImage 3 (S-series)1,198before: 458 rows, user 150 onlyImage 4 (A-series)932before: 373 rows, user 150 only
Show this figure as a table
Rows by artifact and image after the change
ArtifactImage 1Image 2Image 3Image 4
App-update (frosting)6068701,198932
Calendar - Events111329127
Calendar - Calendars55125
Settings Search5555
My Files recents930214280 (279 + 1)142
Figure 4App-update rows by Android user on four Android 16 images, one scale. After the change the artifact returns both users. The other three artifacts changed in the same way; their totals are in the table. Source: pull request body, first checklist item; LAB-TR-0006, Table 7. Aggregate counts only, no synthetic values.

The column states where the database sits, not who used the phone, and user 150 is a Samsung Secure Folder in these images. The change does not show that a second user was used, only that its database was read. The scale of the pattern is bounded, not measured: the Lab counted 67 of 529 artifact module files that take the first element of the file list, many of which are correct for a single system path.

Where this sits in the Lab's research

The measured loss and the doubling test are Section 6 of the Lab’s report on silent failures: Addai, D. (2026). Empty Is Not Absent: Silent Failure Modes in Open-Source Mobile Forensic Parsers on iOS 26 and Android 16. Lab Technical Report LAB-TR-0006, version 1.0, Zenodo, doi:10.5281/zenodo.23717929 (all versions: 10.5281/zenodo.23717928). Read the report on this site. The planned report LAB-TR-0005 on hidden second users was merged into that report as Section 6, so there is no separate LAB-TR-0005 page.

Lab Technical Report LAB-TR-0037 (available on this site as a preprint; Zenodo deposit pending) treats the first-match loss as a measured case of dropped rows and checks how often simple rules would flip when a share of rows is lost. In that report the loss on the three devices was 56.8, 61.8 and 60.0 percent of the rows, and it was by user, not by row.

Sources

Every number and claim above, with where it comes from.

Sources
Number or claimSource
Pull request merged 25 October 2026; 795 additions, 103 deletions, 22 files; 6 synthetic casesPull request metadata (gh pr view)
App-update rows 606, 870, 1,198, 932 (606; 494 + 376; 740 + 458; 559 + 373)Pull request body, first checklist item
Calendar events 111, 3, 291, 27; Calendars 5, 5, 12, 5; Settings Search 5 each; My Files 930, 214, 280, 142Same item; LAB-TR-0006, Table 7
Before: user 150 rows 376, 458, 373; no calendar events or settings search rows on three imagesSame item
1,207 of 3,000 rows returned, none of 1,793 primary-user rowsLAB-TR-0006, Section 6.2
Loss 56.8, 61.8, 60.0 percentLAB-TR-0037, Table 7
Doubling test: six artifacts exactly double; 12 of 12 legs pass; all six second-user legs fail without the changePull request body, third checklist item
677 rows figure replaced by recountPull request body, last checklist item; LAB-TR-0006, Section 6.3
138 passes, 175 storage-view passes, 3 known failures, 6 new unit tests; lint 10.00 of 10Pull request body, "Test record"
24 artifact files call the first-match function; not auditedPull request body, "Anything reviewers should know"
67 of 529 artifact module files take the first elementLAB-TR-0006, Section 6.3
Figures 1 to 3 are diagrams with synthetic values; Figure 4 uses the counts aboveAuthor