Open Source / ALEAPP #1779

Gallery rows kept when the address table is gone

A Samsung Gallery database can keep a view after the table behind it is removed, and the artifact then reported no images at all. This contribution keeps the image rows and says what a blank address means.

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

View the pull request on GitHub

Summary

On a Samsung Android 16 phone the Gallery media store, cmh.db, can keep a view in its schema after the table that the view reads has been removed. A join on that view then fails, and the ALEAPP artifact for the database returned no rows although the database held image rows. This change tests the view first and, when it cannot be queried, keeps the image rows and leaves the Address column blank.

The result on four research images is 5,523, 215, 4,582 and 1,450 rows where the artifact reported 0, 0, 0 and 3. The notes say what a blank Address means and what was observed in the data, so an examiner is not left to guess whether blank columns are a defect.

  • 5,523rows on the largest image, 0 before
  • 4images checked
  • 216lines added, 6 removed
  • 2synthetic test cases

The issue

Imagine an examiner who opens the report for a Samsung phone and finds the Gallery section empty. A phone with no photos and a parser that could not read the photos look the same: a table with no rows. The log holds one line about a missing table, and a log line is easy to miss.

That is what happened with the CMH media store on a One UI 8 database. The artifact selects image rows from the files table and joins location_view to add an address. On the database tested the view was still defined, but the location table it selects from was gone, so the query failed with “no such table: main.location” and the artifact reported nothing, for a database holding 213 image rows. The same failure was found on four images: the artifact returned 0, 0, 0 and 3 rows where the databases held 5,523, 215, 4,582 and 1,450 image rows.

The database schema and the report before and afterLeft: the files table joined to location_view, which selects from a location table that is gone. Right: before the change the report showed 0 rows with the error only in the log; after, 213 rows with Address blank.Schema of the database (diagram)What the report showedfilesimage rows: media_type = 1213 rows on this databaselocation_viewkept in the schemaselects from locationlocationtable is goneLEFT JOIN on _idError in the log: no such table: main.locationBefore0 rows reportedthe error is only in the logAfter213 rows reportedAddress left blanka log line says why
Figure 1The view stayed in the schema but the table it reads was removed, so a join on the view failed and the artifact returned nothing. After the change the image rows are kept. Source: pull request body, "What this changes", and the diff of cmh.py (diagram; the 213 rows are an aggregate count).

The loss is large and silent. It removes every image row, with the date taken, the URI and the path, and it does so because of an optional piece of information, the address. The Lab’s study of silent parser failures calls this class “empty is not absent”: the artifact matched a file, ran, and returned a table that looked like a normal answer. An examiner working from the report alone would conclude there was nothing to examine.

The essence

The artifact now tests the view with a one-row query before it uses it. If the query works, nothing changes: the view is joined on _id and Address is the location text. If the query fails, the artifact writes a log line, leaves out the join and reports the image rows with Address blank. The notes state that a blank Address does not show the photos had no address.

The same notes record what was observed in the data so that blank columns are explained. On the 213-row database, Latitude, Longitude and Is Private held no value on any row. On the 5,523-row database, Date Added, Date Modified, Title and Bucket Name were also empty, while Date Taken, URI and Path were filled on every row. Those values are NULL in the files table; the artifact does not drop them.

Columns of the artifact
ColumnRead fromWhen the view cannot be queried
Date Taken, Date Added, Date Modified, Title, Bucket NamefilesUnchanged
Latitude, Longitude, Is PrivatefilesUnchanged (empty on the tested databases)
Addresslocation_view.address_textBlank, no join
URI, Pathfiles.uri, files._dataUnchanged

Our methodology

How the defect was found. The Lab ran the complete artifact sets of two open-source parser suites on research images and classified every outcome, then traced each zero-row outcome back to the raw store. This artifact matched a file and returned no rows; on the Android image it was the one defect among 143 artifacts in that state, most of which are legitimately empty. The raw store told the real story: a count of image rows in the files table gave 213 on that database. The count, made with SQLite alone and no ALEAPP code, is what separated a defect from an unused feature.

The fix and its form. The change is small on purpose: 25 added and 5 removed lines in the artifact, and the rest is notes and test material. The view is tested first, the failure is logged by name, and the query that follows is built with or without the join. Nothing else in the artifact moves, so databases that keep the location table behave as before.

How the artifact now reads cmh.dbOpen the database, test the view with a one-row query. If it works, join the view and show its address text. If it fails, log a line and set Address to NULL with no join. Then read the files table for images and write eleven columns.Open cmh.dbone per Android userTest the viewSELECT _id FROMlocation_view LIMIT 1Query worksjoin the view on _idAddress = location textQuery failslog a line, no joinAddress = NULLRead filesmedia_type = 1Date Taken, Date Added, Date Modified, Title, Bucket Name, Latitude, Longitude, Address, URI, Path, Is Private
Figure 2The decision the artifact now makes before it joins the view. Both branches end in the same eleven columns. Source: diff of scripts/artifacts/cmh.py (diagram).

Synthetic fixtures. Two cases with invented values were added: one with a files table, a location table and the view, and one in which the view is present and the table is dropped, as in the Samsung database. The repository’s test runner passes both. The pull request records 134 passing cases against 132 before, no failures, and 165 passes in the storage-view mode with five known failures that carry reasons.

Independent recount and second users. On each of the four images the artifact’s row count was compared with a separate count of image rows per user database (Figure 3). A Secure Folder user keeps its own cmh.db, and the artifact reads it beside the primary user’s: the two extra terms in the counts are 2, 21 and 3 rows on three images. The repository’s output checker was run on the four full reports; its first run flagged four columns as empty on all 5,523 rows, and the check against the table showed that they are NULL in the stored data, so the notes now say so.

Rows reported before and after on four imagesFour groups of two bars on one scale from 0 to 6000. Before the change: 0, 0, 0 and 3 rows. After: 5,523, 215, 4,582 and 1,450 rows. Each after value equals an independent count of image rows: 5,523; 213 plus 2; 4,561 plus 21; 1,447 plus 3.After the changeBefore the change01,0002,0003,0004,0005,0006,000Image 15,5230 before; independent count 5,523Image 22150 before; independent count 213 + 2Image 34,5820 before; independent count 4,561 + 21Image 41,4503 before; independent count 1,447 + 3Independent count: SELECT count(*) FROM files WHERE media_type = 1 on each user's database.
Show this figure as a table
Rows before and after
ImageBeforeAfterIndependent count
Image 105,5235,523
Image 20215213 + 2
Image 304,5824,561 + 21
Image 431,4501,447 + 3
Figure 3Rows reported by the artifact before and after the change on four Android 16 images, one scale. The independent count has two terms where a Secure Folder user holds a second database. Source: pull request body, "For a new or changed artifact", first item. Aggregate counts only, no synthetic values.

Known limits, recorded. The storage-view comparison reads a byte-identical copy of a database once, so the two new cases carry a recorded known failure for their second-user leg; it is not a dropped user, because a real second user’s database differs, as the counts show. The automated workflows of the repository needed a maintainer’s approval for a first-time contributor, so the test record in the pull request is the local equivalent of each of them. Older Android versions were not re-run.

The value

For an examiner the change restores a whole class of evidence in the report: the images that the phone’s Gallery indexed, with the date taken, the URI and the path, for each Android user. The question “how many images does this device index, and where do they point?” now has an answer where it had none. The counts matter for scope: 5,523 rows on one device and 215 on another are very different review tasks.

The change also protects against a second mistake, the opposite one. A report with many empty columns can look like a broken parser. Here the empty columns are in the data, and the notes say so. That lets an examiner report that the store holds no title or bucket name for these rows without implying that the tool lost them.

Which columns held a value on the database with 5,523 image rowsEleven columns. Date Taken, URI and Path were filled on all 5,523 rows. Date Added, Date Modified, Title, Bucket Name, Latitude, Longitude and Is Private were empty on all 5,523 rows because the values are NULL in the files table. Address is blank because the location table is gone.Database with 5,523 image rowsDate Takenfilled on all rowsURIfilled on all rowsPathfilled on all rowsDate Addedempty on all rows (NULL in files)Date Modifiedempty on all rows (NULL in files)Titleempty on all rows (NULL in files)Bucket Nameempty on all rows (NULL in files)Latitudeempty on all rows (NULL in files)Longitudeempty on all rows (NULL in files)Is Privateempty on all rows (NULL in files)Addressblank: no table to readThe gaps are in the stored data. The artifact does not drop these columns.
Show this figure as a table
Column coverage on the 5,523-row database
ColumnRows with a valueReason
Date Taken5,523 of 5,523filled
URI5,523 of 5,523filled
Path5,523 of 5,523filled
Date Added0 of 5,523NULL in the files table
Date Modified0 of 5,523NULL in the files table
Title0 of 5,523NULL in the files table
Bucket Name0 of 5,523NULL in the files table
Latitude0 of 5,523NULL in the files table
Longitude0 of 5,523NULL in the files table
Is Private0 of 5,523NULL in the files table
Address0 of 5,523location table is gone
Figure 4Value coverage of the eleven report columns on the largest database. Empty columns reflect the stored data and are named in the artifact notes. Source: notes of cmh.py in the diff and pull request body, second checklist item. Aggregate counts only, no synthetic values.

Limits stay in view. Address is blank because the table is gone, which says nothing about whether the photos carried locations; Latitude and Longitude were empty on every row of the databases tested, which also is not evidence about the photographs. Whether other builds keep addresses in another table was not examined. Dates in the report are the values the database stores. They were not checked against the photographs themselves, and they are indicators to be read with other sources.

Where this sits in the Lab's research

This defect is a case study in the Lab’s report on silent parser 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. Its Table 6 lists the media database defect with the counts used here. The report finds that no single check catches every silent failure and that a raw count taken from the store is one that does catch this one.

Lab Technical Report LAB-TR-0037 (available on this site as a preprint; Zenodo deposit pending) injects this class of failure, an empty or dropped read, into synthetic phones and shows how often a simple count check exposes it. No Lab report covers the Address behaviour on other builds; for that 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 25 October 2026; 216 additions, 6 deletions, 7 files, 2 synthetic casesPull request metadata (gh pr view)
213 image rows; error text "no such table: main.location"Pull request body, "What this changes"
Rows after: 5,523, 215, 4,582, 1,450; before: 0, 0, 0, 3Pull request body, first checklist item
Independent counts 5,523; 213 + 2; 4,561 + 21; 1,447 + 3Same item
Four columns empty on all 5,523 rows; Date Taken, URI and Path filled; Latitude, Longitude, Is Private emptyNotes of cmh.py in the diff; pull request body
25 lines added and 5 removed in the artifactDiff statistics of scripts/artifacts/cmh.py
134 passes, 165 storage-view passes, 5 known failures; lint 10.00 of 10Pull request body, "Test record"
Workflows awaiting approval; local equivalent runPull request body, last row of the test record
Media database defect: 0, 0, 0 and 3 rows; 0 of 213 rows on the sweep image; one defect among 143 matched-no-rows artifacts on Android; the raw count separated it from empty casesLAB-TR-0006, Table 6 and Sections 5.3 and 5.4
Empty-read class measured as 1 to 5,523 stored rows reported as zero on 3 of 4 imagesLAB-TR-0037, Table 7
Figures 1 and 2 are diagrams; Figures 3 and 4 use the counts aboveAuthor