Open Source / iLEAPP #2517

Camera and microphone sessions in the unified log

The camera capture daemon writes a line when a session starts. This contribution lists those lines, together with SpringBoard's recording-indicator lines, and shows why a session line says which process asked, not that anything was recorded.

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

View the pull request on GitHub

Summary

The pull request adds three artifacts to iLEAPP's logarchive module. They list the lines in which the camera capture daemon records a capture session being created, started, changed or stopped; the daemon's streaming power events; and SpringBoard's recording-indicator lines. Fields are copied as logged and no meaning is given to the numbers.

The most useful finding is a warning. On one public-corpus image, every capture-session row belonged to a system daemon that uses the camera for face authentication. None came from the Camera app or a third-party app. A start line is a lead, and it does not establish that audio or video was recorded.

  • 3artifacts added
  • 316lines of parser code
  • 1,656lines added across 7 files
  • 102session rows on one image, all from one system daemon

Methodology note

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

The issue

An examiner asks whether the camera or microphone was used around a time of interest. The unified log has a camera capture daemon that writes a line when a session is created and when a process starts or stops it. It looks like a direct answer.

It is not. On a public-corpus image with iOS 26.5.2, the parser found 102 session rows, and every row that carried an app ID named the same Apple system daemon, which uses the camera for biometric authentication. The other rows were its own lines or lines with the same PID. None came from the Camera app or from a third-party app, even though the import also held lines of the Camera app and of a third-party app in shapes the artifact does not collect. A search that took every line from the camera daemon as camera use would have reported activity that was face authentication.

Counting lines goes wrong in a second way. In the iOS 27.0 log that the notes describe, torch lines from the same daemon outnumbered the session lines by a wide margin. A count of lines from that daemon is not a count of camera use.

Who wrote the capture-session rowsBar chart on one scale from 0 to 80. Of 102 capture-session rows on one research image, 78 carry an app ID that names the system face-authentication daemon, 24 are lines with the same PID or written by that process, and none come from the Camera app or a third-party app.Session rows on one research image, by who wrote them (102 rows)020406080App ID names the face-authentication daemon78No app ID: same PID or its own lines24Camera app0Third-party app0Rows are log lines, not sessions. Image: iOS 26.5.2, a public-corpus image.
Show this figure as a table
Session rows by writer
GroupRows
App ID names the face-authentication daemon78
No app ID: same PID or its own lines24
Camera app0
Third-party app0
Figure 1A reader who counted these rows as camera use would be wrong: every one belongs to the face-authentication daemon. Source: notes of logarchive_capture_sessions in the pull request diff. Real counts; no synthetic values.

The essence

The contribution reads the daemon's session lifecycle, its streaming power events and SpringBoard's recording-indicator lines, and copies each field as logged. A start line is shown as a lead. State lines are listed only when the started flag or a stolen flag is 1, and idle state lines are collected but not listed.

The three artifacts
ArtifactLines readFields added
logarchive_capture_sessionsSession create, start and stop, status transition, detach, start and stop written by the app itselfFunction, app ID, PID, session object, started, camera and audio flags, background, stolen flags, errors, status, transition
logarchive_capture_streamingCamera streaming power eventPID, application ID, device type, streaming flag, port type
logarchive_recording_indicatorSpringBoard activity-changed, layer creation, rendering assertionThe whole message as logged

The streaming lines carried the application ID as <private> in the logs read, so the app has to be taken from the PID, for example by matching it with the PID in the session artifact. That match was not validated against a known capture.

Our methodology

Message shapes. The shapes were read on decoded iOS 26.6 and 27.0 sysdiagnose logs, then fixed as literal markers. The collection step requires the subsystem as well as the text, because the same subsystem carried tens of thousands of torch and device-vendor lines that no marker matches (Figure 2).

Synthetic fixture and tests. A synthetic log show export holds every branch, plus decoy lines under other subsystems. Nineteen tests in four groups run it through the real two-stage chain: the run and database, the collection markers, the parser, and the fixture. One test fails if a marker is removed from the collection step. Another checks that torch and vendor lines reach no artifact. A small change to make_test_data.py lets a module whose artifacts declare no paths be cased without raising.

Behavior on missing or odd data. A line in another shape gives blanks rather than a guess, and the full message stays in the row (Figure 3). A flag name inside another word is not read. A transition needs an arrow. The Session Object address can be reused, so the notes tell the reader to compare times as well.

Validation on real images. Run on two corpus sysdiagnoses, the counts are in sample_data: 102 session rows and 12 streaming rows on the iOS 26.5.2 image, none on the iOS 26.6 image, where the import held 1,140 lines of the camera subsystem and none in these shapes. The recording-indicator artifact gave 1 row on iOS 26.6, an empty activity set, and none on iOS 26.5.2, where all 51 indicator-prefix lines were opacity or force-layer lines. App capture, the audio flag and Mic Stolen at 1 are exercised only by the synthetic fixture, and the notes say so.

From log lines to three artifactsThe log is filtered by subsystem and by literal markers in the collection step into the table logarchive_artifacts. Three artifacts read that table: capture sessions with 18 columns, camera streaming events with 10 columns and recording indicator lines with 5 columns.Unified logsubsystem com.apple.cameracapture / SpringBoardCollection stepsubsystem AND markerslogarchive_artifacts()Tablelogarchive_artifactsTorch, vendor and ISP linesnever match a marker, sothey are not collectedlogarchive_capture_sessions18 columnslifecycle lines, flags as loggedlogarchive_capture_streaming10 columnspower events: PID, device, portlogarchive_recording_indicator5 columnsSpringBoard indicator lines
Show this figure as a table
Artifacts and their columns
ArtifactColumns
logarchive_capture_sessions18
logarchive_capture_streaming10
logarchive_recording_indicator5
Figure 2One collection step, three artifacts. The subsystem condition keeps the torch and device-vendor lines out. Source: pull request diff: logarchive_artifacts(), CAPTURE_*_HEADERS, RECORDING_INDICATOR_HEADERS. Diagram.
A message read into report columnsSeven parts of one synthetic message, each with an arrow to the report column or columns it fills: function, session object with PID and app ID, started flag, camera and audio flags, background flag, stolen flags and session status.One synthetic capture-daemon line, read into columnscaptureSession_updateRunningConditionLogged Function<0x7f0b1c00[4101][com.example.fakecam]>Session Object, PID, App IDClientStartedSession:1Started Session flagCam/Audio:1/0Cam/Audiobg:0BackgroundmicStolen:0 cameraStolen:0Mic Stolen, Camera StolensessionStatus:RunningSession StatusInvented values. Numbers are copied as logged; no meaning is assigned to them.
Show this figure as a table
Message parts and columns
Message partColumn
captureSession_updateRunningConditionLogged Function
<0x7f0b1c00[4101][com.example.fakecam]>Session Object, PID, App ID
ClientStartedSession:1Started Session flag
Cam/Audio:1/0Cam/Audio
bg:0Background
micStolen:0 cameraStolen:0Mic Stolen, Camera Stolen
sessionStatus:RunningSession Status
Figure 3What the parser does with one line: regular expressions copy each field, and a line in another shape gives blanks instead of a guess. Source: regular expressions in the pull request diff (CAPTURE_TAG, CAPTURE_STATE, CAPTURE_STATUS); message text is a synthetic example.

The value

Who benefits. Examiners who see capture-session lines in a sysdiagnose and need to know whose they are, and researchers comparing what different iOS builds log.

Which question can now be answered. “Which process asked the capture daemon for a session, and when?” On the iOS 26.5.2 image the answer was a system daemon, and Camera Stolen read 1 on 6 rows, all lines of that daemon. The artifact copies the flag and does not say what it means.

What it changes in a case. An examiner can treat a session row as an indicator tied to a named process and a time, and place it beside the streaming and indicator rows. Without it, a reader sees only a count of lines.

Limits. A start line is the daemon's record of a request, not of frames recorded. Microphone capture that does not pass through this daemon is not collected. An empty result is not evidence that nothing was captured.

Coverage of the capture artifactsMatrix of seven kinds of line against three outcomes. Lifecycle and started or stolen state lines are collected and listed. State lines with all flags at zero are collected but not listed. Torch, device-vendor and ISP lines and audio sessions are not collected.What reaches a report rowCollected and listedCollected, not listedNot collectedSession create, start, stop, detachState line, started or stolen flag is 1State line, all three flags 0Torch (FigFlashlightLocal) linesDevice-vendor lines (usage counts, timers)ISP subsystem linesAudio sessions, calls, voice recordingLast row: not covered by this daemon at all, so a count of its lines is not a count of camera or microphone use.
Show this figure as a table
Coverage by kind of line
Kind of lineOutcome
Session create, start, stop, detachCollected and listed
State line, started or stolen flag is 1Collected and listed
State line, all three flags 0Collected, not listed
Torch linesNot collected
Device-vendor linesNot collected
ISP subsystem linesNot collected
Audio sessions, calls, voice recordingNot covered
Figure 4The artifact holds back what it cannot interpret. Filled boxes mark the outcome of each kind of line. Source: notes and WHERE fragments of logarchive_capture_sessions in the pull request diff. Diagram.

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” (preprint, not peer reviewed; available on this site, Zenodo deposit pending, so there is no DOI yet). The report counts the camera and microphone indicators as a partial trace, lists capture sessions among its nine marker families, and uses the biometric daemon's sessions as a look-alike that a marker must not count as app capture (its Table 6 and Table 7). The report's point that most controls leave a trace and few record a change applies here: a session line is a trace of the daemon's request, not a record of a change in anything the user set.

The companion white paper LAB-TR-0017 (preprint on this site; Zenodo deposit pending) maps where security settings live on disk and does not cover capture sessions. 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
3 artifacts; 18, 10 and 5 columnsPull request diff: CAPTURE_SESSION_HEADERS, CAPTURE_STREAMING_HEADERS, RECORDING_INDICATOR_HEADERS
316 lines added to logarchive.py; 1,656 lines added in 7 files; merged 20 October 2026Pull request metadata (files, additions, mergedAt)
102 session rows, 78 with an app ID naming the daemon, 24 other rows; none from the Camera app or a third-party appnotes of logarchive_capture_sessions and sample_data in the diff; pull request body
12 streaming rows (6 with streaming 1, 6 with 0) on iOS 26.5.2; 0 on iOS 26.6notes and sample_data of logarchive_capture_streaming in the diff
1 indicator row on iOS 26.6; 0 on iOS 26.5.2, where 51 indicator-prefix lines were opacity or force-layer linesnotes and sample_data of logarchive_recording_indicator in the diff
Camera Stolen read 1 on 6 rowsnotes of logarchive_capture_sessions; pull request body
1,140 lines of the camera subsystem on iOS 26.6, none in these shapesnotes of logarchive_capture_sessions in the diff
19 tests in four groupsCount of test functions in test_logarchive_capture_sessions.py in the diff
Torch lines outnumbered session lines in the iOS 27.0 lognotes of logarchive_capture_sessions in the diff (no count given)
Camera and microphone indicators as a partial trace; biometric sessions as a look-alikeLAB-TR-0012, Tables 4, 6 and 7