Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ xcuserdata/**/*
!xcuserdata/**/xcschemes/*

*.moved-aside
LocalSigning.xcconfig
57 changes: 51 additions & 6 deletions Daemon/SCDaemon.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "SCDaemonXPC.h"
#import"SCDaemonBlockMethods.h"
#import "SCFileWatcher.h"
#import "SCBlockClock.h"

static NSString* serviceName = @"org.eyebeam.selfcontrold";
float const INACTIVITY_LIMIT_SECS = 60 * 2; // 2 minutes
Expand All @@ -25,6 +26,7 @@ @interface SCDaemon () <NSXPCListenerDelegate>

@property (nonatomic, strong, readwrite) NSXPCListener* listener;
@property (strong, readwrite) NSTimer* checkupTimer;
@property (strong, readwrite) NSTimer* checkpointTimer;
@property (strong, readwrite) NSTimer* inactivityTimer;
@property (nonatomic, strong, readwrite) NSDate* lastActivityDate;

Expand Down Expand Up @@ -100,11 +102,26 @@ - (void)stopCheckupTimer {
if (self.checkupTimer == nil) {
return;
}

[self.checkupTimer invalidate];
self.checkupTimer = nil;
}

- (void)startCheckpointTimer {
if (self.checkpointTimer != nil) return;
self.checkpointTimer = [NSTimer scheduledTimerWithTimeInterval: 30.0
repeats: YES
block: ^(NSTimer* _Nonnull t) {
[SCBlockClock tickCheckpoint];
}];
}

- (void)stopCheckpointTimer {
if (self.checkpointTimer == nil) return;
[self.checkpointTimer invalidate];
self.checkpointTimer = nil;
}
Comment on lines +110 to +123
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

startCheckpointTimer is never wired into the daemon lifecycle.

I can't find a call site for this helper anywhere in Daemon/SCDaemon.m, so the new checkpoint path never starts and tickCheckpoint never runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Daemon/SCDaemon.m` around lines 110 - 123, The new checkpoint timer helper
methods (startCheckpointTimer and stopCheckpointTimer) are never invoked, so
SCBlockClock.tickCheckpoint never runs; call startCheckpointTimer from the
daemon startup path (e.g., the method that initializes/starts the daemon, such
as the startup/start method in SCDaemon) so the timer is scheduled when the
daemon comes up, and call stopCheckpointTimer from the daemon shutdown/teardown
path (e.g., stop, dealloc, or shutdown handler) to invalidate the timer on exit;
ensure you reference the existing startCheckpointTimer and stopCheckpointTimer
methods and keep calls symmetric to avoid leaks.



- (void)startInactivityTimer {
self.inactivityTimer = [NSTimer scheduledTimerWithTimeInterval: 15.0 repeats: YES block:^(NSTimer * _Nonnull timer) {
Expand Down Expand Up @@ -133,6 +150,10 @@ - (void)dealloc {
[self.checkupTimer invalidate];
self.checkupTimer = nil;
}
if (self.checkpointTimer) {
[self.checkpointTimer invalidate];
self.checkpointTimer = nil;
}
if (self.inactivityTimer) {
[self.inactivityTimer invalidate];
self.inactivityTimer = nil;
Expand All @@ -155,15 +176,39 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne
if (SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(guestAttributes), kSecCSDefaultFlags, &guest) != errSecSuccess) {
return NO;
}

SecRequirementRef isSelfControlApp;

// Pin XPC clients to the same Apple Developer Team that signed this daemon.
// This mirrors the SMAuthorizedClients plist entry and works for both Apple
// Development local builds and Developer ID release builds (both certs have subject.OU).
NSString* teamID = nil;
SecCodeRef selfCode = NULL;
if (SecCodeCopySelf(kSecCSDefaultFlags, &selfCode) == errSecSuccess) {
CFDictionaryRef signingInfo = NULL;
if (SecCodeCopySigningInformation(selfCode, kSecCSSigningInformation, &signingInfo) == errSecSuccess) {
teamID = (__bridge NSString*)CFDictionaryGetValue(signingInfo, kSecCodeInfoTeamIdentifier);
teamID = [teamID copy];
CFRelease(signingInfo);
}
CFRelease(selfCode);
}
if (teamID.length == 0) {
NSLog(@"Rejecting XPC connection: could not determine daemon's own team identifier");
CFRelease(guest);
return NO;
}

NSString* requirementString = [NSString stringWithFormat:
@"anchor apple generic and (identifier \"org.eyebeam.SelfControl\" or identifier \"org.eyebeam.selfcontrol-cli\") and info [CFBundleVersion] >= \"407\" and certificate leaf[subject.OU] = \"%@\"",
teamID];

SecRequirementRef isSelfControlApp = NULL;
// versions before 4.0 didn't have hardened code signing, so aren't trustworthy to talk to the daemon
// (plus the daemon didn't exist before 4.0 so there's really no reason they should want to run it!)
SecRequirementCreateWithString(CFSTR("anchor apple generic and (identifier \"org.eyebeam.SelfControl\" or identifier \"org.eyebeam.selfcontrol-cli\") and info [CFBundleVersion] >= \"407\" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = EG6ZYP3AQH)"), kSecCSDefaultFlags, &isSelfControlApp);
SecRequirementCreateWithString((__bridge CFStringRef)requirementString, kSecCSDefaultFlags, &isSelfControlApp);
OSStatus clientValidityStatus = SecCodeCheckValidity(guest, kSecCSDefaultFlags, isSelfControlApp);
Comment on lines +200 to 208
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What does macOS SecCodeCheckValiditydo when theSecRequirementRefargument isNULL, and should callers reject the connection if SecRequirementCreateWithString fails?

💡 Result:

When the SecRequirementRef argument is NULL in a call to SecCodeCheckValidity, the function does not impose any additional code requirements beyond the standard signature validation [1][2]. It will still perform dynamic validation of the signed code, including verifying the code's signature and ensuring that the code object's host is valid [1]. Essentially, passing NULL tells the system to verify that the code is properly signed and valid, but not to enforce specific identity or entitlement constraints [1]. Regarding the failure of SecRequirementCreateWithString, callers should absolutely reject the connection (or the operation) if this function fails [3]. SecRequirementCreateWithString is responsible for compiling the text representation of your security requirements into a SecRequirement object [4]. If it fails (returns a non-zero OSStatus), it means the requirement string is invalid or could not be parsed [4][5]. Proceeding without a successfully created requirement—or worse, falling back to a NULL requirement when you intended to enforce specific security constraints—would bypass the intended security checks, leaving the application vulnerable to unauthorized or malicious code [3][6]. Always check the return status of security-critical functions and handle errors by aborting the operation [7][3].

Citations:


🏁 Script executed:

find . -name "SCDaemon.m" -type f

Repository: SelfControlApp/selfcontrol

Length of output: 90


🏁 Script executed:

sed -n '195,215p' ./Daemon/SCDaemon.m

Repository: SelfControlApp/selfcontrol

Length of output: 1307


Fail closed if building the client requirement fails.

If SecRequirementCreateWithString returns an error here, isSelfControlApp stays NULL and the subsequent validity check stops enforcing your bundle/team restriction. That turns this into a fail-open auth path. The initial signature validation in SecCodeCheckValidity will pass for any properly signed code, completely bypassing the identity constraints you intended to enforce.

🔒 Possible fix
     SecRequirementRef isSelfControlApp = NULL;
-    // versions before 4.0 didn't have hardened code signing, so aren't trustworthy to talk to the daemon
-    // (plus the daemon didn't exist before 4.0 so there's really no reason they should want to run it!)
-    SecRequirementCreateWithString((__bridge CFStringRef)requirementString, kSecCSDefaultFlags, &isSelfControlApp);
+    // versions before 4.0 didn't have hardened code signing, so aren't trustworthy to talk to the daemon
+    // (plus the daemon didn't exist before 4.0 so there's really no reason they should want to run it!)
+    OSStatus requirementStatus =
+        SecRequirementCreateWithString((__bridge CFStringRef)requirementString,
+                                       kSecCSDefaultFlags,
+                                       &isSelfControlApp);
+    if (requirementStatus != errSecSuccess || isSelfControlApp == NULL) {
+        NSLog(@"Rejecting XPC connection: could not build client requirement (%d)", (int)requirementStatus);
+        CFRelease(guest);
+        return NO;
+    }
     OSStatus clientValidityStatus = SecCodeCheckValidity(guest, kSecCSDefaultFlags, isSelfControlApp);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Daemon/SCDaemon.m` around lines 200 - 208, The code currently ignores the
return value of SecRequirementCreateWithString so isSelfControlApp may be NULL
and SecCodeCheckValidity then becomes ineffective; update the block around
requirementString/SecRequirementCreateWithString to check the OSStatus result,
and if it is not errSecSuccess fail closed by logging/erroring and setting
clientValidityStatus to a failing status (or returning/aborting) before calling
SecCodeCheckValidity; ensure you reference SecRequirementCreateWithString,
isSelfControlApp and clientValidityStatus and free/CFRelease isSelfControlApp
only when created successfully.


CFRelease(guest);
CFRelease(isSelfControlApp);
if (isSelfControlApp) CFRelease(isSelfControlApp);

if (clientValidityStatus) {
NSError* error = [NSError errorWithDomain: NSOSStatusErrorDomain code: clientValidityStatus userInfo: nil];
Expand Down
2 changes: 1 addition & 1 deletion Daemon/selfcontrold-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<string>Free and open-source under the GPL.</string>
<key>SMAuthorizedClients</key>
<array>
<string>anchor apple generic and (identifier &quot;org.eyebeam.SelfControl&quot; or identifier &quot;org.eyebeam.selfcontrol-cli&quot;) and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = EG6ZYP3AQH)</string>
<string>anchor apple generic and (identifier &quot;org.eyebeam.SelfControl&quot; or identifier &quot;org.eyebeam.selfcontrol-cli&quot;) and certificate leaf[subject.OU] = &quot;$(DEVELOPMENT_TEAM)&quot;</string>
</array>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<key>SMPrivilegedExecutables</key>
<dict>
<key>org.eyebeam.selfcontrold</key>
<string>anchor apple generic and identifier &quot;org.eyebeam.selfcontrold&quot; and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = EG6ZYP3AQH)</string>
<string>anchor apple generic and identifier &quot;org.eyebeam.selfcontrold&quot; and certificate leaf[subject.OU] = &quot;$(DEVELOPMENT_TEAM)&quot;</string>
</dict>
<key>SUEnableAutomaticChecks</key>
<true/>
Expand Down
5 changes: 5 additions & 0 deletions LocalSigning.xcconfig.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copy this file to LocalSigning.xcconfig and put your Apple Developer
// Team ID below. Find your Team ID at developer.apple.com → Membership.
// This file is gitignored so you won't accidentally commit your team ID.

DEVELOPMENT_TEAM = YOUR_TEAM_ID_HERE
Loading