Open Source / iLEAPP #2574

Three parser fixes for empty and failed reports

When a parser fails, the report goes quiet in a way that looks like an unused feature. Three small fixes make one calendar, one location-services list and one app read what the files hold, and the checks behind them show how to tell a real empty from a failed read.

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

View iLEAPP pull request #2574 on GitHub

Summary

  • Three iLEAPP artifacts returned an error or nothing on recent iPhone images, and the reports looked like an unused feature.
  • One item with a stored time that prints as year 0000 stopped the whole Calendar Events artifact; a renamed key left the location configuration artifact at zero rows; a sidecar file confused the Waze reader.
  • The fixes recover 866 calendar rows on one image and 5 location rows on each of eight, and change nothing else in 46 compared report files.
  • The pull request was merged on 25 October 2026.
  • 3defects fixed
  • 866calendar rows recovered on one image
  • 46report files byte-identical elsewhere
  • +592 / -37lines changed

The issue

An examiner runs iLEAPP over a full file system image and looks at the report. The calendar is not there. The log shows an error, "year 0 is out of range", and the Calendar Events artifact has no table at all. On the same image the calendar database holds 866 events. A single item with a stored creation time of -63114076800 seconds was enough: SQLite prints that value as year 0000, Python cannot hold a date before year 1, and the first such row stopped the whole artifact.

That failure is at least loud. The other two are not. A location-services artifact that reads one entry of the locationd configuration file returned no rows on every image we tried, because the entry is keyed differently on recent iOS releases. A missing table of this kind is identical to a phone that never used the feature. And the Waze artifacts logged "file is not a database" because a search pattern that ends in user.db* also matches the -wal and -shm sidecar files, and the parser took whichever one the archive listed first.

The examiner's problem is the same in all three cases. An empty or absent table is read as "the phone holds nothing", and the report does not say that the parser could not read what was there. Figure 1 shows the three cases and what each returns now.

Three parsers, before and after the fixThree panels. Calendar Events failed with a year-out-of-range error and now returns 866 rows on that image. The location services configuration artifact returned 0 rows and now returns 5 on all eight images. The Waze artifacts logged a database error 8 times and now log none.Calendar EventsBeforeWhole artifact fails:year 0 is out of rangeno table in the reportAfter866 rows returned5 creation times left blankLSC - application list fileBefore0 rows on eight imagesthe entry was not foundlooks like an unused featureAfter5 rows on all eight imagesboth key forms are readWaze (user.db)Before“file is not a database”logged 8 times on one imageno rowsAfter0 log errorsrows unchanged: PLACES is emptyCounts are from the pull request (iOS 26.3.1 image for Calendar Events, iOS 26.6.2 image for Waze).
Show this figure as a table
Before and after for the three fixes
FixBeforeAfter
Calendar Eventserror, no table866 rows, 5 blank creation times
Location services configuration0 rows5 rows on all eight images
Waze8 log errors0 log errors
Figure 1The three parsers before and after the fix, with the number each returns. Source: PR #2574 body, checklist item 1 (aggregate counts, no synthetic values needed).

The essence

Three small changes, each tied to one mechanism, and nothing else changes in the reports. In Calendar Events the four time conversions leave a value that Python cannot hold blank instead of raising. In the location-services artifact the Routine.bundle entry is read under both of its key forms. In the Waze artifacts a small helper returns the main database file when it is present and nothing when only sidecars are present.

On eight real images the pull request compared 46 report files between the branch and upstream main and found them byte-identical, so no other artifact output changes.

The three changes
ArtifactFile changedFields touchedWhat changes
Calendar EventscalendarAll.pycreation, start, end and modification timesA time SQLite renders as year 0000 is left blank; the row is kept
Location services configurationlocServicesconfig.pyRoutine.bundle entry, five keysBoth key forms are read: the older com.apple.locationd.bundle-<path> and the newer p<path>:
Waze, five artifactswaze.pyuser.db and its sidecarsThe main database file is returned whatever the order; a lone sidecar returns nothing

Our methodology

The three defects were found by running every module over iPhone full file system extractions from the Lab's research corpus and looking at what returned nothing or failed. For each artifact that matched a file but returned no rows, or that raised an error, we compared it with the raw store: the row count of the table, or the presence of the key the artifact reads. An empty table is recorded as expected. A table that holds rows the artifact did not return, a key present under another name, or an exception on a stored value is recorded as a defect.

Each defect then went through the same steps. We reproduced it on a synthetic file, wrote the fix, and added a test that fails without it. A small builder creates a Calendar database and a configuration file with invented values, and no device data is used in the fixtures. The pull request adds three unit-test files with eight tests, and four registered test cases (three Calendar tables and the configuration artifact). On upstream main the Calendar Events case errors with "year 0 is out of range" and the configuration case returns no rows; on the branch all four pass.

Row counts on the real images were taken with separate code before and after. The independent count of non-Gregorian calendar items on the failing image is 866, and 5 of them carry the year-0000 time. On the other seven images the Calendar Events rows are identical to main. The configuration artifact returns 5 rows on all eight images against 0 before; every one of the eight files holds only the newer key form. For Waze, upstream main logged the database error 8 times on the iOS 26.6.2 image and the branch logs it 0 times (Figures 2 and 3).

Two points are stated as limits in the pull request. The Waze fix is verified on one image and one failure mode, because it is a search-order effect: the tar, directory and backup readers were not exercised with a sidecar listed first. And no Waze test case is possible in the repository's harness, which hands artifacts a mock file seeker, so the order is covered by a unit test and by the real-image run. The Calendar fix leaves the time blank rather than guessing a date; creation time was the only column seen to carry the value, with start, end and last-modified times valid on all five items.

What was stored, what the parser did, and the fixThree rows, one per defect, each with four boxes: the stored value, what the parser did, the result in the report, and the fix.StoredWhat the parser didIn the reportThe fixcreation_date-63114076800SQLite prints year 0000Python datetime holdsyears 1 to 9999ValueError on that rowWhole artifact failsno table at all_utc_or_blank():an unreadable timebecomes a blank cellRoutine.bundle entry,newer key form:p<path>:Looked up only theolder key form:locationd.bundle-<path>0 rowssame as an unused featureROUTINE_KEYS: read bothkey formsuser.db, user.db-wal,user.db-shmzip lists -shm firstPattern user.db* tookthe first hit: the-shm file“file is not adatabase” loggedno rowsSearch all matches, returnthe main file, or nothingif only sidecars existValue shown is the stored number from the pull request; no other value is reproduced.
Show this figure as a table
Mechanism of each defect
FixStoredWhat the parser didFix
Calendar Eventscreation_date -63114076800, printed as year 0000Python datetime holds years 1 to 9999; ValueError_utc_or_blank leaves it blank
Location services configurationnewer key p<path>:looked up only the older keyROUTINE_KEYS holds both
Wazeuser.db, -wal, -shm; -shm listed firstpattern user.db* took the first hitreturn the main file or nothing
Figure 2For each defect: the stored value, what the parser did with it, how it showed in the report and the fix (schematic; the only value shown is the stored number from the pull request). Source: diff files calendarAll.py, locServicesconfig.py and waze.py (diagram, synthetic example of the mechanism).
Calendar Events rows per image, upstream main against this branchBar chart for eight iPhone images. On seven images both versions return the same rows. On the eighth image upstream main fails and the branch returns 866 rows.Calendar Events rows per imageupstream mainthis branchImage with the year-0000 itemupstream main: error, no rows866Other image 11,348Other image 2191Other image 3204Other image 4149Other image 51,578Other image 6141Other image 714304008001,2001,600
Show this figure as a table
Calendar Events rows per image
ImageUpstream mainThis branch
Image with the year-0000 itemerror866
Other image 11,3481,348
Other image 2191191
Other image 3204204
Other image 4149149
Other image 51,5781,578
Other image 6141141
Other image 7143143
Figure 3Calendar Events rows per image. Seven images are unchanged; the eighth fails on upstream main and returns 866 rows on the branch. Source: PR #2574 body, checklist item 1 (aggregate counts, no synthetic values).

The value

The fixes restore three results an examiner would otherwise have missed: the whole calendar on one image, the Routine.bundle timing entries on all eight images, and a clean log for Waze. The wider lesson is in how they were found. A parser written for one release can return an empty table when a later release renames a key or stores a value outside the range of the language, and the report gives no sign of it. Counting rows in the raw store, as the checks here do, separates a real empty from a failed read.

It is easy to over-read the numbers. In the sweep behind this work, 322 iOS artifacts had no rows or an error on at least one of two images, and only two were drift defects, with a third that changed only the log (Figure 4). That is a lower bound on what the sweep found, not a rate, and it says that most empty results were genuinely empty. The right habit is to count before concluding, not to distrust every blank.

The limits are specific. The location configuration artifact writes all five rows whenever the entry exists, so a blank value can mean the key was not present: three of the five rows are blank on three of the eight images. Versions before iOS 18 are not covered by the newer-key finding in the pull request, and the iOS 18.1.1 check is carried over from an earlier local run. An artifact that now returns a row has still not shown that a person used the feature.

How many empty or failed results were defectsBar chart: 322 iOS artifacts had no rows or an error on at least one image; 2 of them were drift defects and 1 changed only the log.iOS artifacts with no rows or an error on at least one of two imagesNo rows or an error322Drift defects found2Changed only the log1A lower bound, not a rate: the other cases were checked against table counts and found empty.
Show this figure as a table
Outcomes among 322 artifacts
GroupArtifacts
No rows or an error on at least one image322
Drift defects2
Changed only the log1
Figure 4Most empty or failed results were not defects, which is why each one is counted against the raw store. Source: LAB-TR-0006 section 5.3 (aggregate counts, no synthetic values).

Where this sits in the Lab's research

This pull request is one of the fixes reported in Lab Technical Report LAB-TR-0006, Empty Is Not Absent: Silent Failure Modes in Open-Source Mobile Forensic Parsers, published on Zenodo (doi:10.5281/zenodo.23717929, version 1.0). The report runs the complete artifact sets of an iOS and an Android parser suite, classifies every outcome and traces zero-row outcomes to the raw stores. Calendar Events is its example of a loud failure, the location-services list of a silent zero, and Waze of a noisy but benign read.

Lab Technical Report LAB-TR-0037, a fault-injection study on synthetic phones, is available on this site as a preprint; the Zenodo deposit is pending. It measures how far a silently empty or truncated read changes a stated conclusion, and it lists the location-services application list among the real cases of an empty read (zero rows on 8 of 8 images). The same pull request is cited in LAB-TR-0006 as the source of the fixes.

Sources

Every figure in the text and in the diagrams is listed here with where it comes from. Nothing else was used: no device, image or report was opened for this post.

Every number and claim in this post, with where it comes from
Number or claimSource
3 fixes; +592 lines, -37 linesPR #2574 file list and body
Merged 25 October 2026PR #2574 state and mergedAt
creation_date -63114076800 prints as year 0000; first such row fails the artifactPR #2574 body; diff file calendarAll.py
866 rows; 5 items with year 0000; independent count 866PR #2574 body, checklist item 1; LAB-TR-0006 Table 6
Calendar rows on the other seven images: 1,348; 191; 204; 149; 1,578; 141; 143PR #2574 body, checklist item 1
5 rows on all eight images against 0; all eight files hold only the newer keyPR #2574 body; diff file locServicesconfig.py
Waze: 8 log errors before, 0 after, iOS 26.6.2; PLACES table emptyPR #2574 body; LAB-TR-0006 Table 6
46 report TSVs byte-identical between branch and upstream main on eight imagesPR #2574 body, first section
3 unit-test files, 8 tests; 4 registered test cases; main errors, branch passesPR #2574 body, test record; diff files test_calendar_year_zero.py, test_loc_services_clients.py, test_waze_user_db_search.py
Waze verified on one image and one failure mode; harness uses a mock seekerPR #2574 body, reviewer notes and test record
3 of 5 location rows blank on 3 of 8 imagesPR #2574 body, checklist item 2
iOS 18.1.1 carried over from an earlier run; versions before iOS 18 not coveredPR #2574 body, checklist item 1
322 iOS artifacts with no rows or an error; 2 drift defects; 1 log-onlyLAB-TR-0006 section 5.3
Location-services application list: zero rows on 8 of 8 imagesLAB-TR-0037, table of real cases (preprint on this site; Zenodo deposit pending)
Zenodo DOI 10.5281/zenodo.23717929 (version 1.0)papers/zenodo/DOIS.md