Open Source / ALEAPP #1780

Blocked numbers and Android Auto rejected cars

A phone can hold a list of blocked numbers and a record of cars it has been set up with, and both were easy to miss. ALEAPP had no reader for the first, left half of the Android Auto car database unread, and read only one database when a phone had more than one Android user. This change fixes all three and says where we have seen no rows.

  • Daniel Addai, SectraTEK Lab
  • Merged
  • Merged 25 October 2026
  • CC BY 4.0

Summary

This contribution adds two artifacts to ALEAPP and corrects a third. Blocked Numbers reads the table in which Android keeps the numbers a user blocked. Android Auto - Rejected Cars reads the second car table in the Android Auto database. The existing Android Auto - Allowed Cars artifact now reads every Android Auto database it finds, one per Android user, where before it read the first match only.

The research images gave an uneven picture, and this post reports it as it was. Blocked Numbers held rows on three of four Android 16 images. Allowed Cars held rows on one of four. Rejected Cars held no row on any image we examined, so its row handling has only been exercised on constructed data.

  • 2New artifacts
  • 1Existing artifact changed
  • +677 / -20Lines changed in 18 files
  • 7New synthetic test cases

The issue

Two questions come up in phone examinations: did the user block a number, and has this phone been set up with a particular car. Both have a home in the Android system, and both were gaps in the report.

Blocked numbers live in the database of the Android BlockedNumberProvider. ALEAPP had no artifact for it, so a report simply had no entry. A reader could not tell “no numbers were blocked” from “this parser does not look”. The list is short, so the cost was not volume but silence.

The Android Auto car database had two separate problems. First, carservicedata.db holds an allowedcars table and a rejectedcars table with the same columns, and the existing artifact read only the first; its own notes said so. Second, the artifact took the first matching database. A phone with a second Android user, for example a secure-folder user, keeps a second copy of the app data under another user number. The artifact read one copy and dropped the other without a message. The pull request puts it plainly: the existing artifact silently dropped a second user's database. A report that shows no error and fewer rows than the phone holds is the failure that is hardest to notice.

Figure 1 shows the mechanism with invented values: two Android users, each with a car database.

Android Auto car databases of two Android users, before and afterLeft: two Android users each hold a carservicedata.db; the Allowed Cars artifact read only the first match and never read rejectedcars. Right: both databases are read and feed two artifacts, Allowed Cars and Rejected Cars, each with a Source File column. Synthetic example.Before: first match onlyAfter: every database, both tablesAndroid user 0carservicedata.dballowedcars: 2 rowsrejectedcars: 2 rowsAndroid user 150carservicedata.dballowedcars: 1 rowrejectedcars: 1 rowAndroid user 0carservicedata.dballowedcars: 2 rowsrejectedcars: 2 rowsAndroid user 150carservicedata.dballowedcars: 1 rowrejectedcars: 1 rowAllowed Cars2 rows, no Source FileNot read:rejectedcars table andthe second userAllowed Cars3 rows + Source FileRejected Cars3 rows + Source FileA report with no error and fewer rows than the phone holds.Source File names the copy each row came from.Row counts in this diagram are invented to show the mechanism.
Show this figure as a table
Figure 1 as a table
ViewAndroid user 0Android user 150What the report shows
Beforeread (2 allowed rows)not readAllowed Cars only, no Source File
Afterread (2 allowed, 2 rejected)read (1 allowed, 1 rejected)Allowed Cars 3 rows and Rejected Cars 3 rows, each with Source File
Figure 1The same phone viewed through the old and the new artifact. Dashed lines mean the data was present and not read. Source: ALEAPP pull request 1780 (diff of androidauto.py and the synthetic cases); row counts are a synthetic example.

The essence

Blocked Numbers reports the blocked table as stored: an ID, the number as entered, and its E.164 form, plus the file the row came from. The table has no timestamp, so the artifact has no time column and says so in its notes. Android Auto - Rejected Cars reads the rejectedcars table with the same code and the same 13 columns as Allowed Cars.

Both Android Auto artifacts now read every carservicedata.db found, once per Android user, with duplicate storage views of one copy collapsed to one, and add a Source File column. A database without the table reports no rows instead of an error, and a blocked-numbers database from an older schema without an e164_number column is read with that column blank.

Artifacts and fields added or changed
ArtifactSource tableReport fieldsTime column
Blocked Numbers (new)blocked in blockednumbers.dbID, Original Number, E.164 Number, Source Filenone; the table has no timestamp
Android Auto - Rejected Cars (new)rejectedcars in carservicedata.db12 car and head unit fields, Source File (13 columns)connectiontime, Unix milliseconds shown in UTC
Android Auto - Allowed Cars (changed)allowedcars in carservicedata.dbsame 13 columns; every database read; Source File addedconnectiontime, as above

Our methodology

We started from the file, not the artifact list. For each store we opened the database read-only, listed its tables and columns, and compared the schema across the images we had. For the car database that showed rejectedcars with the same columns as allowedcars in every copy examined: 20 copies in 11 research images, with 16 to 24 columns depending on the app version. For blocked numbers it showed the same three-column table on all 50 copies in 34 research images.

The meaning of a column came from the Android source, not from guesswork. The artifact notes cite the AOSP BlockedNumberProvider: unblocking a number deletes its row, and _id is an auto-increment key that is not reused. So a gap in the ID sequence is consistent with rows having been removed, and the notes add that the table keeps no record of what a missing ID held. On the first Android 16 image the IDs ran from 1 to 126 without a gap. On the third and fourth they reached 612 and 960 over 561 and 958 rows. In the same notes, six numbers of five or six digits had an empty E.164 value and the other 120 had a plus-prefixed one; the notes call that an observation on one database, not a rule for the column.

Checks came in four kinds. Five synthetic cases with invented values cover two Android users plus a duplicate storage view, an older schema without e164_number, an empty table, a car database with both tables, and one without rejectedcars. An independent count(*) on the source tables, with none of the parser's code, gave the same row counts on all four images, and the eight connectiontime values were all 13-digit integers, read as Unix milliseconds. The tool's output checker found nothing to look at on the full reports. And a doubling test placed the same databases under a second Android user: Blocked Numbers read 1,122 rows from 561, and Allowed Cars 16 from 8, exactly double. The suite passed 139 cases with no failure, seven more than before the change.

Figure 2 shows the steps; Figure 3 shows the counts and the doubling test.

How the two stores are read, from file to report columnsTwo lanes. Blocked Numbers: blockednumbers.db, collapse storage views, read the blocked table, report with four columns. Android Auto: carservicedata.db, collapse storage views, check the table exists then read allowedcars or rejectedcars, report with thirteen columns.Blocked NumbersSource fileblockednumbers.dbone per Android userin user_de storageCollapse viewsunique_files()storage views of onecopy become oneRead the tableFROM blocked_id, original_number,e164_numberabsent column: blankReport columnsID, Original Number,E.164 Number,Source Fileno time columnAndroid Auto carsSource filecarservicedata.dbone per Android userin the gearhead appCollapse viewsunique_files()storage views of onecopy become oneCheck, then readtable present first;none: no rows, no errorFROM allowedcars orFROM rejectedcarsReport columnsconnectiontime in UTC,12 car columns,Source File13 columns in allconnectiontime is read as Unix milliseconds. The table name is all the database says about why a row is there.Diagram of the parser steps in androidauto.py and blockedNumbers.py; no values are shown.
Show this figure as a table
Figure 2 as a table
StepBlocked NumbersAndroid Auto cars
Source fileblockednumbers.db, one per Android usercarservicedata.db, one per Android user
Collapse viewsunique_files()unique_files()
ReadSELECT from blocked; missing e164_number reads blankcheck sqlite_master, then SELECT from allowedcars or rejectedcars
ReportID, Original Number, E.164 Number, Source Fileconnectiontime, 11 car and head unit columns, Source File (13 columns)
Figure 2From file to report. Both lanes share the same first two steps; the Android Auto lane adds a check that the table exists. Source: ALEAPP pull request 1780, scripts/artifacts/blockedNumbers.py and androidauto.py (diagram of the steps).
Row counts for Blocked Numbers and Allowed Cars, and the second-user doubling testBar chart on one scale from 0 to 1,200. Blocked Numbers rows on four Android 16 images: 126, 0, 561 and 958. Doubling test: the same databases placed under a second Android user read exactly double, 561 to 1,122 and 8 to 16.03006009001,200Rows read by the artifactBlocked Numbers, rows on four Android 16 research imagesImage 1126Image 20Image 3561Image 4958Doubling test: the same database copied under a second Android userBlocked, one user561Blocked, two users1,122Allowed Cars, one user8Allowed Cars, two users16
Show this figure as a table
Figure 3 as a table
MeasureValue
Blocked Numbers rows, image 1 to 4126, 0, 561, 958
Blocked Numbers, one user to two users (image 3)561 to 1,122
Allowed Cars, one user to two users8 to 16
Figure 3Row counts from four Android 16 research images, and the doubling test on a tree built from real database copies. Grey is the single-user read, blue the read with a second user added. Source: ALEAPP pull request 1780 body (test record and independent count(*)); counts are real aggregates from the research images, not synthetic.

The value

An examiner can now answer two questions from the report instead of from a manual query. Which numbers are on the Android block list, with their stored and E.164 forms? And which cars does the Android Auto app hold in each of its two tables, for every Android user on the phone? A second user's cars no longer vanish from the report, and a reader can see which copy a row came from.

The limits matter as much as the new rows. The artifact reads the Android provider's list only; the Samsung dialer keeps its own block list, which is not read. A block list has no time, so the report cannot say when a number was blocked, and a number missing from it was not necessarily never blocked. For the cars, the app is closed source: what places a car in either table, and what event connectiontime marks, is not established, so a row is an indicator of a record in the app, not proof of a completed connection. An empty rejected-cars table is not evidence that no car was ever rejected. Figure 4 lists where rows were, and were not, seen.

Where the research images held rowsCoverage matrix. Rows: Blocked Numbers, Allowed Cars, Rejected Cars. Columns: four Android 16 images with rows per image, and aggregate counts from other research images. Rejected Cars held no row anywhere.Android 16 images, rows per imageOther research images (aggregate)Image 1Image 2Image 3Image 4Blocked Numbers12605619582 of 34 images hold rows50 copies, none with a second userAllowed Cars0no database8015 rows in 8 of 11 imagesthat hold the databaseRejected Cars0no database00table present in 20 copiesin 11 images, no rowsrows were readdatabase or table present, no rows, or no databaseCounts are real aggregates from the Lab's research images; no device or value is identified.
Show this figure as a table
Figure 4 as a table
ArtifactImage 1Image 2Image 3Image 4Other research images
Blocked Numbers12605619582 of 34 images hold rows; 50 copies
Allowed Cars0no database8015 rows in 8 of 11 images
Rejected Cars0no database0020 copies in 11 images, no rows
Figure 4Honest coverage. Rejected Cars was never seen with a row, so its row handling rests on constructed data. Source: ALEAPP pull request 1780 body and the artifact notes in the diff (real aggregate counts, not synthetic).

Where this sits in the Lab's research

No Lab report is devoted to these stores yet; the method above comes from the pull request. They appear as case studies in Lab Technical Report LAB-TR-0006, Section 7.1 (counts per image and the empty rejected-cars table) and Section 6 (the doubling test). That report states that the Android Auto artifacts had the same first-match pattern and that no real image held a second user's Android Auto database, so the second-user case is shown on constructed data only.

Cite as: Addai, D. (2026). Empty Is Not Absent: Silent Failure Modes in Open-Source Mobile Forensic Parsers on iOS 26 and Android 16. LAB-TR-0006, version 1.0, preprint. doi:10.5281/zenodo.23717929.

Sources

Every number and claim on this page, and where it comes from. Figures marked synthetic example use invented values.

Sources for the numbers and claims in this post
Number or claimWhere it comes from
Merged 25 October 2026; 18 files, +677 and -20 linesPull request 1780 metadata (mergedAt, files, additions, deletions)
Blocked Numbers 126 / 0 / 561 / 958 rows; Allowed Cars 0 / no database / 8 / 0; Rejected Cars 0 / no database / 0 / 0PR body, test record bullet 1; LAB-TR-0006 Table 8
Independent count(*) gives the same numbers; eight connectiontime values are 13-digit integersPR body, test record bullet 1
Doubling test: 561 to 1,122 and 8 to 16PR body, bullet 3; LAB-TR-0006 Section 6.2
20 copies of carservicedata.db in 11 images, rejectedcars present, no rows, 16 to 24 columnsDiff, notes of extract_android_auto_rejected in androidauto.py
50 copies of blockednumbers.db in 34 images, two images hold rows (1 and 6), none with a second userDiff, notes of get_blocked_numbers in blockedNumbers.py
15 rows in 8 of 11 images; 8 rows on one further Android 16 imageDiff, notes of extract_android_auto in androidauto.py
IDs 1 to 126 without gap; 612 and 960 over 561 and 958 rows; 6 empty and 120 plus-prefixed E.164 valuesPR body, test record row Independent check of the notes; blockedNumbers.py notes
Unblocking deletes the row; _id is AUTOINCREMENTblockedNumbers.py notes, citing the AOSP BlockedNumberProvider source
run_test_cases.py PASS=139, upstream PASS=132, seven new casesPR body, test record table
Samsung dialer's own block list is not readPR body, section Anything reviewers should know
Five synthetic cases with invented values; users 0 and 150 in the fixtureDiff, admin/test/cases/testdata.blockedNumbers.json and testdata.androidauto.json
Figures 1 and 2 row counts and stepsFigure 1: synthetic example; Figure 2: diagram of androidauto.py and blockedNumbers.py