Watch your background: Scheduling XProtect Remediator scans
Many of the background activities managed by Duet Activity Scheduler and Centralised Task Scheduling, the DAS-CTS system, are largely invisible to the user, as they’re services supporting other services. Two of the best-known activities are automatic Time Machine backups and XProtect Remediator scans for malware. I first came to learn about DAS-CTS eight years ago when investigating problems with backups in macOS Sierra, and have written extensively here about how they work. This article concentrates instead on XProtect Remediator scans, which are more recent and an important part of security defence in macOS.
XProtect Remediator (XPR) appears, and to a degree can work, like an app, located in /Library/Apple/System/Library/CoreServices/XProtect.app. Look inside its bundle and in the MacOS folder you’ll see its code alongside 24 scanning modules. These are run through LaunchAgents and LaunchDaemons property lists in its Resources folder. Once a day XPR runs a paired set of scans to detect and remove or remediate known malicious software. These are scheduled and dispatched by the DAS-CTS system, and understanding how they work helps diagnose and address problems that can arise with XPR scans.
Launch Events
XPR contains two key property lists setting up its scans, com.apple.XProtect.agent.scan.plist and com.apple.XProtect.daemon.scan.plist. These are essentially the same, the first to run scans as the current user, and the second as root. They define three types of Launch Events, which for the user are:
a fast scan, com.apple.XProtect.PluginService.agent.fast.scan, performed at intervals of 6 hours (21600 seconds), and run when on battery;
a standard scan, com.apple.XProtect.PluginService.agent.scan, performed at intervals of 24 hours (86400 seconds), but not run when on battery;
a slow scan, com.apple.XProtect.PluginService.agent.slow.scan, performed at intervals of 7 days (604800 seconds), but not run when on battery.
These are assigned a ProcessType of Background, so are scheduled and dispatched by the DAS-CTS system.
What the scans do
In the standard scan, each of the scanning modules is run in turn, once using the agent version running as the current user, normally 501, and once using the daemon version as root, user 0.
You’re unlikely to see any evidence of a fast scan at the moment, although they should be run every 6 hours. Fast scans only run specific scan modules designated by Apple as meriting such frequent checks. This was used during a period of high threat soon after XPR was first introduced, but as far as I’m aware hasn’t been used since. Currently, when a fast scan is run, each scanning module in turn is reported as not being required to run, and those single-line entries are the only records left in the log. That could of course change in response to a change in threat.
As a slow scan is only run once a week, they’re hard to locate in the log. So far I haven’t discovered any.
The results from standard scans are thus the only XPR activities you’re likely to see in the logs, and using XProCheck.
You can read relevant log entries using the DAS Scheduling log extract feature in Mints, or a generic log browser like Ulbow using the filter predicate
subsystem == “com.apple.duetactivityscheduler” OR subsystem CONTAINS “com.apple.xpc”
Registration
During startup, XPR’s property lists are read and its activities are registered by DAS-CTS. For example, this series of log entries reports the LaunchAgent for standard scans being registered following user login:
CTS Creating on XPC add event: com.apple.XProtect.PluginService.agent.scan
CTS Created: com.apple.XProtect.PluginService.agent.scan (0x9cc8652c0)
DAS Submit activity: <private> in group: <private> with capacity: 1
CTS Submitting: 501:com.apple.XProtect.PluginService.agent.scan:0B9197 (CTS Activity 0x9cc8652c0)
DAS SUBMITTING: 501:com.apple.XProtect.PluginService.agent.scan:0B9197
CTS Registered: com.apple.XProtect.PluginService.agent.scan (0x9cc8652c0)
DAS Submitted: 501:com.apple.XProtect.PluginService.agent.scan:0B9197 at priority 30 with interval 86400 (Sun Jan 26 23:23:56 2025 – Mon Jan 27 23:23:56 2025)
Its priority setting is different from the Quality of Service (QoS) set in its property lists, which is Utility, matching a QoS raw number of 17, not 30. That’s common, and demonstrates how QoS values set in the API aren’t the same as those used internally in macOS.
At this stage, DAS makes its first assessment as to whether to dispatch this activity. As this occurs within five minutes of starting up, DAS decides it Must Not Proceed, MNP. The Optimal Score for that activity is reported, and set as the minimum score it must achieve before DAS will dispatch it to be run.
DAS 501:com.apple.XProtect.PluginService.agent.scan:0B9197:[
{name: Boot Time Policy, policyWeight: 0.010, response: {33, 0.00, [{[Minimum seconds after boot]: Required:300.00, Observed:39.00},]}}
{name: Device Activity Policy, policyWeight: 20.000, response: {33, 0.00, [{deviceActivity == 1}]}}
], Decision: MNP}
DAS <private>: Optimal Score 0.6872 at <private> (Valid Until: <private>)
Dispatch
DAS rescores activities in its list at frequent intervals. When an XPR activity, here the LaunchDaemon version of XPR’s standard scan, exceeds the threshold, DAS makes the decision that the activity Can Proceed, CP.
DAS 0:com.apple.XProtect.PluginService.daemon.scan:847936:[ ], Decision: CP Score: 0.980125}
DAS ‘0:com.apple.XProtect.PluginService.daemon.scan:847936’ CurrentScore: 0.980125, ThresholdScore: 0.587700 DecisionToRun:1
DAS With <private> …Tasks pre-running in group [com.apple.dasd.default] are 1!
DAS 501:com.apple.XProtect.PluginService.agent.scan:0B9197:[ ], Decision: CP Score: 0.980125}
Some activities, presumably those identified as CPUIntensive or DiskIntensive in their property lists, are then checked against others for compatibility. This is marked by a sequence of those checks, such as
DuetAS ‘501:com.apple.XProtect.PluginService.agent.scan:0B9197’ has compatibility score of -1.000000 with 0:com.apple.XProtect.PluginService.daemon.scan:847936 (Started at <Not yet started>). Bailing out.
In this case, it ensures that user and root scans aren’t run at the same time.
Once DAS is happy that starting this scan is desirable and compatible, it then requests CTS to start the activity.
DAS REQUESTING START: 0:com.apple.XProtect.PluginService.daemon.scan:847936
CTS DAS told us to run com.apple.XProtect.PluginService.daemon.scan (0x61e2fc140)
CTS evaluating activities
CTS com.apple.XProtect.PluginService.daemon.scan state change 1 -> 2
CTS Initiating: com.apple.XProtect.PluginService.daemon.scan (0x61e2fc140)
CTS _xpc_activity_dispatch: beginning dispatch, activity name com.apple.XProtect.PluginService.daemon.scan, seqno 0
CTS _xpc_activity_dispatch: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0): found an activity with matching seqno 0
CTS _xpc_activity_begin_running: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0) seqno: 0.
CTS _xpc_activity_dispatch: lower half, activity name com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), seqno from top half was 0
CTS _xpc_activity_dispatch: created connection 0x123004ac0 for activity name com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), seqno 0
CTS _xpc_activity_set_state: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), 2
CTS _xpc_activity_set_state: send new state to CTS: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), 2
CTS Running (PID 2351): com.apple.XProtect.PluginService.daemon.scan (0x61e2fc140)
DAS STARTING: <private>
CTS _xpc_activity_set_state_from_cts: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), set activity state to 2
CTS XPC_ACTIVITY_CALLING_HANDLER__: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), current state 2, pending state 0
CTS _xpc_activity_set_state: com.apple.XProtect.PluginService.daemon.scan (0x6000035200a0), 4
DAS then records that the activity started:
DAS STARTING <_DASActivity: “0:com.apple.XProtect.PluginService.daemon.scan:847936”, Utility, 60s, [26/01/2025, 23:23:54 – 27/01/2025, 23:23:54], Started at 27/01/2025, 12:05:36, Group: com.apple.dasd.default, Intensive: CPU Disk, PID: 2351>!
DAS Activity <private> has preventDeviceSleep 0. PluggedIn state: 1
DAS With <private> …Tasks running in group [com.apple.dasd.default] are 3!
DAS Started <private>, total runtime from previous runs 0.0 mins
DAS 0:com.apple.XProtect.PluginService.daemon.scan:847936:[ ], Decision: CP Score: 0.980125}
Diagnosis
The only common problem encountered in running XPR is an apparent failure to run daily scans. Relevant causes are:
Insufficient log records, that don’t cover sufficient time to capture a set of scans.
Running a notebook on battery, as standard scans won’t be dispatched unless the Mac is running on mains power.
Running other CPU- or disk-intensive tasks, which may cause scans to be deferred until those are completed.
High thermal load, which will cause scans to be deferred until thermal conditions have improved. This is the origin of Duet in Duet Activity Scheduler, referring to the CoreDuet system responsible for monitoring the Mac’s operating environment and managing tasks appropriately.
Failure of DAS to score activities in its list, currently extremely improbable, but can only be diagnosed from its log entries.
Other failures in DAS-CTS, which again require log analysis.
One of the difficulties in diagnosis is that there’s no command tool that can inspect DAS activity lists, making log inspection essential in most cases.