Open Source / iLEAPP #2311

Reading URL opens from the unified log

iOS writes a line to the unified log when something opens a link, but it withholds the link itself. This contribution teaches iLEAPP to list those lines, so an examiner can say that a link was opened, when, and which process wrote the line.

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

View the pull request on GitHub

Summary

The pull request adds one artifact, logarchive_urlopens, to the logarchive module of iLEAPP. It lists three kinds of unified log line: SpringBoard dispatching a URL-open action, a UIKit scene receiving a URL, and the launch service reporting that no app is registered for a URL scheme. Each row is one log line, not one open.

iOS usually replaces the URL with <private>. The artifact therefore answers “was a link opened, when, and which process wrote the line”. It does not answer “which link”. A maintainer review found that the first version would return an empty report in every real run; the fix and the notes that state what a row means are described below.

  • 3log markers read
  • 54lines added, in 1 file
  • 4research images in the notes
  • 1review round that blocked the merge

Methodology note

Drafted with AI assistance and reviewed by the author before publication.

The issue

An examiner is asked whether a phone opened a link at about a certain time, for example from a widget or from another app. The unified log does hold traces of that: SpringBoard writes a URL-open action, a UIKit scene reports that it received a URL, and the launch service complains when no app handles a scheme. Before this contribution iLEAPP had no artifact for these lines. The examiner had to export the log and search it by hand.

The lines are also hard to read. iOS replaces the value of a dynamic argument with <private> unless the developer marks it public, and in most of these lines the URL is one of those arguments. A line says that something was opened. It rarely says what. One open can also write several lines, so counting lines overstates the opens.

The first version of the contribution had a second problem, found in review, and it is a useful one to know about. iLEAPP's dependent logarchive_* artifacts do not read the full log table. They read a smaller table that a collection step fills from its own list of markers. The first version listed the three markers only in the artifact's filter, so the collection step never kept those lines and the artifact returned no rows, without an error. The maintainer built a few lines to show it: the same filter returned all three lines from the full log table and none from the table the artifact reads. The review also suspected that the first counts in the description came from querying the full table and asked for a run of iLEAPP itself.

Three log lines before and after the artifactOn the left, three synthetic unified log lines: a SpringBoard URL-open action, a scene receiving a URL, and a failed scheme lookup. On the right, the report before the contribution has no rows for them and the report after lists one row per line.Log lines (synthetic example)10:02:11.120 SpringBoardUIOpenURLAction ... url: <private>10:02:11.201 MobileNotesscene:openURLContexts:] opening url context <private>10:05:40.002 lsdno registered handler for URL scheme <private>BeforeReport: no rows, no artifactThese lines were in the log but not listedAfter10:02:11.120 SpringBoard UIOpenURLAction ...10:02:11.201 MobileNotes openURLContexts ...10:05:40.002 lsd no registered handler ...Invented times and lines; the real URL value is withheld by iOS as <private>.
Show this figure as a table
Synthetic log lines and the rows they become
TimeProcessMarkerURL value
10:02:11.120SpringBoardUIOpenURLActionprivate
10:02:11.201MobileNotesopenURLContextsprivate
10:05:40.002lsdno registered handler for URL schemeprivate
Figure 1The same three lines, before and after. The artifact lists lines; it does not recover the URL. Source: the three markers in the pull request diff (logarchive.py); line text and times are a synthetic example.

The essence

The artifact reads the unified log lines that match one of three literal markers and copies the standard log columns. It adds no URL and no inference. Its notes say, for each marker, what a line shows and what it leaves out. Lines that ask whether a scheme could be opened (-canOpenURL:) are checks, not opens, and are deliberately not matched.

The three markers and what a row means
MarkerTypically written byWhat a row says
UIOpenURLActionSpringBoard, and the app that receives the actionA URL-open action was sent or received. Most lines hold no URL.
openURLContextsAn app or settings process (Preferences on the iOS 17.2.1 image), from the UIKit scene methodA scene was handed a URL. The value was <private> on the tested images. The name is broad and may match lines that are not opens on other images.
no registered handler for URL schemeThe launch service (lsd)A scheme had no registered app. The line names neither the requesting process nor whether the request was an open or a check.

The Process Image Path column is the process that wrote the line, not always the app that received the URL. On the iOS 26.6 image SpringBoard wrote 4 of the 5 rows, and the receiving app is named inside the message.

Our methodology

Finding the markers. The three strings were taken from decoded sysdiagnose logs, as literal message text. A subsystem name alone was not used, because a name says which component wrote a line and not what the line records. The artifact follows the existing dependent pattern: no file paths of its own, a requirement to run logarchive first, and a SQL fragment that filters the collected table (Figure 2).

Checking against real logs. The description first reported a count on two research sysdiagnoses. The maintainer's review is the reason the method reads as it does now. The collection step was fixed, and the final notes record what the artifact returned on four research images (Figure 3): URLs appear unevenly, one message ended at 1,029 characters with no closing bracket, so a longer line would be cut in the parsed log, and the other action lines held no URL at all.

Saying only what was tested. The review asked that the redaction claim be limited to the tested images, that the notes say a row is a line and not an open, and that someone look at what the broad openURLContexts marker also picks up. The final notes do the first two, and record that one image gave no matching line. They state that the marker can match non-opens on other images.

Behavior when data is absent. If no line matches, the report has no rows. The notes say this plainly, and the empty iOS 17.3 result in Figure 3 is kept in the sample data so a reader can see it.

Pipeline from log to report rowsFlow in two rows. The unified log is imported by the logarchive module into a table. The collection step keeps lines that match a marker in a second table. The URL-open artifact filters the second table with the same three markers and writes one report row per line.Unified logtracev3 files or a log exportread from a sysdiagnoselogarchive moduleimports every recordtable: logarchiveCollection steplogarchive_artifacts()keeps lines that match a marker3 markers added after reviewTablelogarchive_artifactsonly the collected lineslogarchive_urlopenssame three markers as filterpaths: NoneReport rowsone row per log linestandard log columns
Show this figure as a table
Pipeline stages
StageWhat it does
logarchive moduleImports every log record into table logarchive
logarchive_artifacts()Copies lines that match any marker into table logarchive_artifacts; the three URL-open markers were added here after review
logarchive_urlopensFilters logarchive_artifacts with the same three markers
ReportOne row per log line
Figure 2Why the first version was empty: the artifact reads the second table, and the collection step, not the artifact, decides which lines reach it. Source: pull request diff, logarchive_artifacts() and logarchive_urlopens(); review comment of the maintainer. Diagram.
Rows per research imageBar chart on one scale from 0 to 6. The iOS 14.3 image gave 1 row, 17.2.1 gave 2, 17.3 gave 0 and 26.6 gave 5.Rows returned by the artifact, per research image0123456iOS 14.3 image1iOS 17.2.1 image2iOS 17.3 image0 (no matching line)iOS 26.6 image5Rows are log lines, not opens. Counts from sample_data in the pull request.
Show this figure as a table
Rows per research image
ImageRows
iOS 14.31
iOS 17.2.12
iOS 17.30
iOS 26.65
Figure 3Few rows, and one image with none. The counts show the markers match rarely, which is why each row is worth reading in full. Source: sample_data of logarchive_urlopens in the pull request diff. Real counts; no synthetic values.

The value

Who benefits. Examiners who need to place a link open in time without reading raw log text, and researchers who want to know which processes write these lines on which iOS versions.

Which question can now be answered. “Did something on this phone try to open a link at about this time, and which process wrote the line?” The answer is an indicator. The notes advise correlating a row with other evidence, such as Safari history or network destinations, to resolve the target.

What it changes in a case. A time-stamped entry that did not exist in the report before. A widget launch, for example, now shows up as a few rows within a fraction of a second (Figure 4), and an examiner who knows that will not report it as three separate link opens.

Limits. A row is a log line. It does not prove that a person tapped a link, that a page loaded, or that an app acted on the link. The URL is usually missing, and the redaction claim holds only for the images tested. The log has a limited window, so an empty result does not show that nothing was opened. The artifact was tested through a sysdiagnose; the notes do not claim the same behavior for other ways of reaching the log.

One open can produce three rowsThree boxes for three log lines logged within 0.16 seconds on the iOS 26.6 research image: a widget launch request, a SpringBoard line and a MobileNotes line. Each becomes its own report row, so three rows describe what appears to be one launch.Three lines logged within 0.16 seconds0.16 sWidget launch requestcarries a UIOpenURLActionURL in clear, non-http schemeSpringBoard linesends UIOpenURLActionto the Notes sceneMobileNotes linereceives the actionwriter is also the receiverRow 1Row 2Row 3Three rows, probably one launch. A row count is not a count of opens.
Show this figure as a table
Three rows, one launch
RowLine
1Widget launch request carrying a UIOpenURLAction
2SpringBoard sends UIOpenURLAction to the Notes scene
3MobileNotes receives it
Figure 4On the iOS 26.6 image, three lines seem to describe one Notes widget launch. The lines are listed in the order the notes name them, not by time. Source: notes of logarchive_urlopens in the pull request diff. Diagram; no synthetic values beyond the box labels.

Where this sits in the Lab's research

This contribution supports Lab Technical Report LAB-TR-0012, “What the iOS Unified Log Says About Device Security Controls: Most Leave a Trace, Few Record a Change” (Addai, Tetteh, Adjabeng and Egyir, 2026; preprint, not peer reviewed; available on this site, Zenodo deposit pending, so there is no DOI yet). In the report, URL opens are the first of nine marker families (Section 3.4), and what the trace withholds and what it keeps is listed in its Table 5. The report's main finding is that most controls leave a trace and few record a change. A URL open fits that pattern: the line records that an open took place and which process wrote it, and not the target.

No other Lab report covers this contribution. The method above is from the pull request.

Sources

Every number in this post is listed here with where it comes from. Figures use these numbers or invented example values, marked as such.

Sources for every number and claim in this post
Number or claimWhere it comes from
Three markers read by the artifactPull request diff, scripts/artifacts/logarchive.py, function logarchive_urlopens() and logarchive_artifacts()
54 lines added, 1 file changed, merged 14 October 2026Pull request metadata (files, additions, mergedAt)
Rows per research image: 1 (iOS 14.3), 2 (17.2.1), 0 (17.3), 5 (26.6)sample_data of logarchive_urlopens in the diff
Three lines within 0.16 seconds; SpringBoard wrote 4 of 5 rows; message ended at 1,029 charactersnotes of logarchive_urlopens in the diff
First version returned no rows; filter returned 3 of 3 constructed lines on the full table and none on the collected tableReview comment by the maintainer, state CHANGES_REQUESTED
Four commits; the last merged main and renamed and rewrote the artifactPull request commit list (the last title is truncated in the list)
Lines for -canOpenURL: are not matchedPull request body and notes in the diff
URL opens as the first of nine marker families; what a URL-open trace withholds and keepsLAB-TR-0012, Section 3.4 and Table 5
Numbers 88 (iOS 26.6) and 24 (iOS 26.2) in the first description and in LAB-TR-0012 Table 6Not used for artifact output in this post: the maintainer suspected they were counts of the full log table, and the final notes give other counts