Swift Privileged Helper (XPC Listener) Crashing with Illegal Instruction Error - swift

I’ve created a Swift macOS app which uses SMJobBless to create a helper with escalated privileges. This works fine—the helper gets installed to /Library/Privileged Helper Tools and an accompanying LaunchDaemon gets created in /Library/LaunchDaemons. However, the helper is unable to start successfully. Instead, it crashes with an “Illegal instruction: 4” message.
I’ve prepared the helper to respond to XML connections by implementing the NSXPCListenerDelegate protocol. Here‘s my Helper main.swift code:
import Foundation
class HelperDelegate: NSObject, NSXPCListenerDelegate {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with: HelperToolProtocol.self)
newConnection.exportedObject = HelperTool()
newConnection.resume()
return true
}
}
let delegate = HelperDelegate()
let listener = NSXPCListener.service()
listener.delegate = delegate
listener.resume()
The crash occurs on the last line, listener.resume().
I tried to launch the helper app manually from the command line (which is identical to what the LaunchDaemon does) and, again, it crashes with the above error message printed to stdout. I don’t have any more ideas on how to test this for the root cause. My implementation is more than rudimentary, following Apple’s guidlines for implementing XM services. Also, the various posts on SO regarding XML services haven’t helped me in resolving this issue. Has anyone of you tried to create a privileged helper in Swift successfully? BTW, the app is not sandboxed.
For the sake of completeness, here’s the code for the HelperTool class referenced in my HelperDelegate class above:
import Foundation
class HelperTool: NSObject, HelperToolProtocol {
func getVersion(withReply reply: (NSData?) -> ()) {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString" as String) as? String ?? "<unknown version>"
let build = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String ?? "<unknown build>"
if let d = "v\(version) (\(build))".data(using: .utf8, allowLossyConversion: false) {
reply(d as NSData)
}
}
}
And finally the HelperToolProtocol:
import Foundation
#objc(HelperToolProtocol) protocol HelperToolProtocol {
func getVersion(withReply: (NSData?) -> ())
}
Thanks for any help!

After days of testing I finally found a solution which makes my XPC helper launch correctly and respond to any messages. The problem lies in the last three lines of the main.swift module which currently read
let listener = NSXPCListener.service()
listener.delegate = delegate
listener.resume()
which, as put in the question, make the helper crash immediately upon the very last line.
I took these lines directly from Apple’s Creating XPC Services documentation. Here’s the documentation for the NSXPCListener resume() function:
If called on the service() object, this method never returns. Therefore, you should call it as the last step inside the XPC service's main function after setting up any desired initial state and configuring the listener itself.
The solution is to not call the NSXPCListener.service() singleton object but rather instantiate a new NSXPCListener object using the init(machServiceName:)initializer passing the same Mach service name that is being used on the main app’s XPC connection. As resume() in this case would resume immediately—thus terminating the helper—you have to put it on the current run loop to have it run indeterminately. Here’s the new, working code:
let listener = NSXPCListener(machServiceName: "Privilege-Escalation-Sample.Helper")
listener.delegate = delegate
listener.resume()
RunLoop.current.run()

Related

ApplescriptObjC method with a parameter

When I try to pass a parameter from Swift into an AppleScriptObjC method I get an error in the console saying
unrecognized selector sent to object
I'm not very knowledgeable when it comes to using a Swift-AppleScriptObjC bridge, but I'm using the method set out here.
I can get ApplescriptObjC to return an argument, but the function doesn't work if it has a parameter. So in the below examples, the method/function getPlaylists() works but when I try to call playPlaylist from say the AppDelegate I get the error message.
Applescript file:
script iTunesBridge
property parent : class "NSObject"
to getPlaylists() -- () -> [NSString]
tell application "Music"
set theList to (name of playlists)
return theList
end tell
end getPlaylists
to playPlaylist:plist -- (NSString) -> ()
tell application "Music"
play playlist (plist as string)
end tell
end playPlaylist
end script
Swift file:
import Cocoa
#objc(NSObject) protocol iTunesBridge {
func getPlaylists() -> [NSString]
func choosePlaylist(plist: NSString)
}
I think it's how I've structured playPlaylist in the AppleScript file but I've tried a number of different ways and all result in the same error.
I've found a solution, but have no idea how it works. If anyone could enlighten me I would really appreciate it!
I create two functions in ApplescriptObjC, one to get the playlist, one to set the playlist. In the bridge file, I set a variable that is { get set } and then list the function to set the playlist. In my view controller (where I'm making the changes from) I create an objc dynamic variable which has a get/set. I can then change that variable and it triggers the AppleScript function to change my playlist... Code as such:
Applescript file:
script iTunesBridge
property parent : class "NSObject"
to playlist()
tell application "Music"
return name of current playlist
end tell
end playlist
to setPlaylist:plist -- (NString) -> ()
tell application "Music"
play playlist (plist as string)
reveal current track
end tell
end setPlaylist:
end script
Bridge Swift file:
import Cocoa
#objc(NSObject) protocol iTunesBridge {
var playlist: NSString { get set }
func setPlaylist(plist: NSString)
}
ViewController:
class MainController: NSViewController {
#objc dynamic var currentPlaylist: NSString! {
get { return self.iTunesBridge.playlist }
set(value) { self.iTunesBridge.playlist = value }
}
}
As I say, I have little idea how and why it works and would really appreciate the answer. For instance, even though at no point in swift do I tie var playlist to the function setPlaylist, it knows to connect them. Set must be the magic word...

How can I verify a class method is called using XCTAssert?

I have a service class, I would like to assert 2 things
A method is called
The correct params are passed to that method
Here is my class
protocol OAuthServiceProtocol {
func initAuthCodeFlow() -> Void
func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void
}
class OAuthService: OAuthServiceProtocol {
fileprivate let apiClient: APIClient
init(apiClient: APIClient) {
self.apiClient = apiClient
}
func initAuthCodeFlow() -> Void {
}
func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void {
}
}
Here are my tests
class OAuthServiceTests: XCTestCase {
var mockAPIClient: APIClient!
var mockURLSession: MockURLSession!
var sut: OAuthService!
override func setUp() {
mockAPIClient = APIClient()
mockAPIClient.session = MockURLSession(data: nil, urlResponse: nil, error: nil)
sut = OAuthService(apiClient: mockAPIClient)
}
func test_InitAuthCodeFlow_CallsRenderOAuthWebView() {
let renderOAuthWebViewExpectation = expectation(description: "RenderOAuthWebView")
class OAuthServiceMock: OAuthService {
override func initAuthCodeFlow() -> Void {
}
override func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) {
renderOAuthWebViewExpectation.fulfill()
}
}
}
}
I was hoping to create a local sub class of OAuthService, assign that as my sut and call something like like sut.initAuthCodeFlow() and then assert that my expectation was fulfilled.
I believe this should satisfy point 1. However I cannot access my expectation when attempting to assign it as fulfilled as I get the following error
Class declaration cannot close over value
'renderOAuthWebViewExpectation' defined in outer scope
How can I mark this as fulfilled?
I am following a TDD approach, so I understand my OAuthService would produce a failing test at this point anyway*
I was hoping to create a local sub class of OAuthService, assign that as my sut and call something like like sut.initAuthCodeFlow() and then assert that my expectation was fulfilled.
I would strongly discourage you from using this approach. If your SUT is an instance of the subclass then your test is not truly testing OAuthService, but OAuthService mock.
Moreover, if we think of tests as a tool to:
prevent bugs when code is change
help refactoring and maintenance of the code
then I would argue that testing that calling a certain function calls another function is not a good test. That's harsh, I know, so let me unpack why that's the case.
The only thing it's testing is that initAuthCodeFlow() calls renderOAuthWebView(forService:, queryitems:) under the hood. It doesn't have any assertion on the actual behaviour of the system under test, on the outputs it produces directly or not. If I were to edit the implementation of renderOAuthWebView(forService:, queryitems:) and add some code that would crash at runtime this test would not fail.
A test like this doesn't help with keeping the codebase easy to change, because if you want to change the implementation of OAuthService, maybe by adding a parameter to renderOAuthWebView(forService:, queryitems:) or by renaming queryitems into queryItems to match the capitalization, you'll have to update both the production code and the test. In other words, the test will get in your way of refactoring -changing how the code looks without changing how it behaves- without any extra benefit.
So, how should one test OAuthService in a way that prevents bugs and helps moving fast? The trick is all in testing the behaviour instead of the implementation.
What should OAuthService do? initAuthCodeFlow() doesn't return any value, so we can check for direct outputs, but we can still check indirect outputs, side effects.
I'm making a guess here, but I from your test checking that renderOAuthWebView(forService:, queryitems:) I'd and the fact that it gets an APIClient type as input I'd say it'll present some kind of web view for a certain URL, and then make another request to the given APIClient maybe with the OAuth token received from the web view?
A way to test the interaction with APIClient is to make an assertion for the expected endpoint to be called. You can do it with a tool like OHHTTPStubs or with your a custom test double for URLSession that records the requests it gets and allows you to check them.
As for the presentation of the web view, you can use the delegate patter for it, and set a test double conforming to the delegate protocol which records whether it's called or not. Or you could test at a higher level and inspect the UIWindow in which the test are running to see if the root view controller is the one with the web view.
At the end of the day is all a matter of trade offs. The approach you've taken is not wrong, it just optimizes more towards asserting the code implementation rather than its behaviour. I hope that with this answer I showed a different kind of optimization, one biased towards the behaviour. In my experience this style of testing proves more helpful in the medium-long run.
Create a property on your mock, mutating it's value within the method you expect to call. You can then use your XCTAssertEqual to check that prop has been updated.
func test_InitAuthCodeFlow_CallsRenderOAuthWebView() {
let renderOAuthWebViewExpectation = expectation(description: "RenderOAuthWebView")
class OAuthServiceMock: OAuthService {
var renderOAuthWebViewExpectation: XCTestExpectation!
var didCallRenderOAuthWebView = false
override func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) {
didCallRenderOAuthWebView = true
renderOAuthWebViewExpectation.fulfill()
}
}
let sut = OAuthServiceMock(apiClient: mockAPIClient)
XCTAssertEqual(sut.didCallRenderOAuthWebView, false)
sut.renderOAuthWebViewExpectation = renderOAuthWebViewExpectation
sut.initAuthCodeFlow()
waitForExpectations(timeout: 1) { _ in
XCTAssertEqual(sut.didCallRenderOAuthWebView, true)
}
}

Swift MVVM testing strategy and code coverage questions

I've run into an issue when generating code coverage with Xcode for view models in an MVVM environment.
Our basic setup is the view controller makes requests to the view model, which in turn calls methods on a data manager that talks to web services.
I came up with what I thought was a reasonably elegant way to test the view models by creating a fake data manager that subclasses the actual data manager and overrides the function called by the VM.
The problem is that for this to work, the VM must be part of the app target and the test target. An apparent side effect of this is that code coverage is not generated for items belonging to two or more targets, even though the unit tests pass. Code coverage is enabled in the project.
Here is a excerpted view model:
import Foundation
class BoosViewModel: BaseViewModel {
convenience override init() {
self.init(dataManager: BoosDataManager(), andModel: nil)
}
func getUnlinkedBoos(_ cardType: CardType) {
(dataManager as! BoosDataManager).getUnlinkedBoos(cardType) { result, error in
...stuff happens here...
}
}
}
... and the data manager
class BoosDataManager: DataManager {
static let SharedInstance: BoosDataManager = {
var manager = BoosDataManager()
return manager
}()
func getUnlinkedBoos(_ cardType: CardType = .loyalty, completion: #escaping ((_ result: BoosModel?, _ error: NSError?) -> Void)) {
...stuff happens here...
}
}
...and the test
class BoosViewModelTests: XCTestCase {
func testGetUnlinkedBoosHappyPath() {
class FauxDataManager: BoosDataManager {
override func getUnlinkedBoos(_ cardType: CardType = .loyalty, completion: #escaping ((_ result: BoosModel?, _ error: NSError?) -> Void)) {
...stuff happens here...
}
}
let viewModel = BoosViewModel()
let dataManager = FauxDataManager()
viewModel.dataManager = dataManager
viewModel.getUnlinkedBoos(.loyalty)
XCTAssertTrue(testObserver.updated)
XCTAssertEqual(testObserver.newViewModel.getBoos().count, 1)
}
}
As I noted earlier the unit tests in this scenario complete successfully, but unit coverage does not get generated.
I have older tests where I actually created an external fake data manager class that was used by the test, the class under test is not part of the test target, and coverage works fine.
The drawback to that is that I have to create multiple data managers to handle specific cases for its returns. If I can't encapsulate the classes, I would need to create a bunch of swift data managers, one for each scenario.
That's why I came up with the internal class.
Now, the problem comes in if I remove the view model under test from the testing target. After doing this, I add #testable import BoosApp to the unit test so that the view model under test can be resolved. When I do this, I get the following error:
Could not cast value of type 'BoosTests.BoosViewModelTests.(testGetUnlinkedBoosHappyPath () -> ()).(FauxDataManager #1)' (0x11f673d18) to 'Boos.BoosDataManager' (0x10444b128).
Aug 30 20:43:01 Pay[19025] : Could not cast value of type 'BoosTests.BoosViewModelTests.(testGetUnlinkedBoosHappyPath () -> ()).(FauxDataManager #1)' (0x11f673d18) to 'Boos.BoosDataManager' (0x10444b128).
I'm not sure what I'm missing. Is there a way to make this scenario work, or am I stuck creating multiple data managers outside of the test code?
Ultimately, I figured out the main issue was that the view model and data manager had somehow gotten added to the test target. After removing them from the test target I was able to make a couple of minor changes and everything is running fine. FYI.

CLKComplicationServer initializes an already initialized singleton class CLKComplicationDataSource, although init() is private

To update complications on the watch, I use a singleton class ComplicationController (irrelevant code has been omitted below):
final class ComplicationController: NSObject, CLKComplicationDataSource {
static let shared = ComplicationController() // Instantiate the singleton
private override init() {
super.init()
print("====self: \(self): init")
} // Others can't init the singleton
}
The singleton is created on the watch by the extension delegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
override init() {
super.init()
_ = ComplicationController.shared
}
}
When I launch the watch extension with a breakpoint at the print statement above, the execution breaks and the stack trace is:
When I then execute the print statement in a single step, the debugger shows:
====self: <Watch_Extension.ComplicationController: 0x7bf35f20>: init
When I then continue, the execution breaks again at the same breakpoint, and the stack trace is:
After another single step, the debugger shows:
====self: <Watch_Extension.ComplicationController: 0x7d3211d0>: init
Obviously, the CLKComplicationServer has created another instance of the singleton.
My question is: Did I something wrong or is this a bug? If it is a bug, is there a workaround?
PS: It does not help not to initialize ComplicationController in the ExtensionDelegate. In this case, the 2nd instance is created as soon as ComplicationController.shared is used anywhere in the code.
I found a workaround:
Do not instantiate the singleton in the app, i.e. do not use ComplicationController.shared anywhere.
If you have to call functions in ComplicationController.shared, send a notification, e.g. to the default notification center.
The ComplicationController singleton had to have registered for such notifications, and when it receives one, it has to execute the required function.
This did work for me.

Initial app launch from storyboard fails to call NSDocument init

I've written a macOS Document-type app with a storyboard, using the Xcode template, and somewhere along the line the association between the initial app launch and the document has varied from the expected pattern such that none of the NSDocument initializers I expect are called when the app first launches (but are called every new window thereafter).
I've subclassed all four documented NSDocument initializers, as follows:
public class Simulation: NSDocument {
override init() {
debugPrint("--------------------\(#file)->\(#function) called")
super.init()
}
init(contentsOf: URL, ofType: String) throws {
debugPrint("--------------------\(#file)->\(#function) called")
fatalError()
}
init(for: URL?, withContentsOf: URL, ofType: String) throws {
debugPrint("--------------------\(#file)->\(#function) called")
fatalError()
}
convenience init(type: String) throws {
debugPrint("--------------------\(#file)->\(#function) called, type: \(type)")
self.init()
}
public override class func autosavesInPlace() -> Bool {
debugPrint("--------------------\(#file)->\(#function) called")
return false
}
}
None of the inits exhibit the debugPrint output when the app launches. The app window is created successfully on launch, with no apparent document association.
However, I notice some really odd behavior I can't explain:
Although I've seen no init call, autosavesInPlace is called three times after the app starts on some instance of the document
When I use cmd-N (i.e., File->New and therefore newDocument()) to create a new document, autosavesInPlace is called three more times, and then the document init is executed!
I never see a call to makeWindowControllers()
My NSDocument subclass is named Simulation. The anomaly seems to be that there's some magic in the initial startup that bypasses Simulation.init, but calls it every document+window creation thereafter.
Here are my questions:
Why does the initial launch not call Simulation.init()?
How does autosavesInPlace find an instance of Simulation to call when there's only that initial, seemingly partially constructed window?
In your storyboard, make sure both your Window Controller and its Content View Controller have Is Initial Controller unchecked and Presentation set to Multiple in the Attributes Inspector.
Having Is Initial Controller checked will cause the application to instantiate one window controller before any of the NSDocument/NSDocumentController "magic" happens. Presentation: Multiple should be selected for coherence, although it might not really make a difference.
Also, make sure your document types are properly setup in Info.plist, particularly the NSDocumentClass key (should contain $(PRODUCT_MODULE_NAME).Simulation).
I believe your question about autosavesInPlace is already answered in the comments…