0

Background

It is possible to perform a software-controlled disconnection of the power adapter of a Mac laptop by creating an DisableInflow power management assertion.

Code from this answer to an SO question can be used to create said assertion. The following is a working example that creates this assertion until the process is killed:

#include <IOKit/pwr_mgt/IOPMLib.h>
#include <unistd.h>

int main()
{
    IOPMAssertionID         neverSleep = 0;

    IOPMAssertionCreateWithName(kIOPMAssertionTypeDisableInflow,
                                kIOPMAssertionLevelOn,
                                CFSTR("disable inflow"),
                                &neverSleep);

    while (1)
    {
        sleep(1);
    }
}

This runs successfully and the power adapter is disconnected by software while the process is running.

What's interesting, though, is that I was able to run this code as a regular user, without root privileges, which wasn't supposed to happen. For instance, note the comment in this file from Apple's open source repositories:

// Disables AC Power Inflow (requires root to initiate)
#define kIOPMAssertionTypeDisableInflow                     CFSTR("DisableInflow")
#define kIOPMInflowDisableAssertion                         kIOPMAssertionTypeDisableInflow

I found some code which apparently performs the actual communication with the charger; it can be found here. The following functions, from this file, appears to be of particular interest:

IOReturn 
AppleSmartBatteryManagerUserClient::externalMethod( 
    uint32_t selector, 
    IOExternalMethodArguments * arguments,
    IOExternalMethodDispatch * dispatch __unused, 
    OSObject * target __unused, 
    void * reference __unused )
{
    if (selector >= kNumBattMethods) {
        // Invalid selector
        return kIOReturnBadArgument;
    }

    switch (selector)
    {
        case kSBInflowDisable:
            // 1 scalar in, 1 scalar out
            return this->secureInflowDisable((int)arguments->scalarInput[0],
                                            (int *)&arguments->scalarOutput[0]);
            break;

        // ...
     }

     // ...
}

IOReturn AppleSmartBatteryManagerUserClient::secureInflowDisable(
    int level,
    int *return_code)
{
    int             admin_priv = 0;
    IOReturn        ret = kIOReturnNotPrivileged;

    if( !(level == 0 || level == 1))
    {
        *return_code = kIOReturnBadArgument;
        return kIOReturnSuccess;
    }

    ret = clientHasPrivilege(fOwningTask, kIOClientPrivilegeAdministrator);
    admin_priv = (kIOReturnSuccess == ret);

    if(admin_priv && fOwner) {
        *return_code = fOwner->disableInflow( level );
        return kIOReturnSuccess;
    } else {
        *return_code = kIOReturnNotPrivileged;
        return kIOReturnSuccess;
    }

}

Note how, in secureInflowDisable(), root privileges are checked for prior to running the code. Note also this initialization code in the same file, again requiring root privileges, as explicitly pointed out in the comments:

bool AppleSmartBatteryManagerUserClient::initWithTask(task_t owningTask, 
                    void *security_id, UInt32 type, OSDictionary * properties)
{    
    uint32_t            _pid;

     /* 1. Only root processes may open a SmartBatteryManagerUserClient.
      * 2. Attempts to create exclusive UserClients will fail if an
      *     exclusive user client is attached.
      * 3. Non-exclusive clients will not be able to perform transactions
      *     while an exclusive client is attached.
      * 3a. Only battery firmware updaters should bother being exclusive.
      */
    if ( kIOReturnSuccess !=
            clientHasPrivilege(owningTask, kIOClientPrivilegeAdministrator))
    {
        return false;
    }

    // ...
}

Starting from the code from the same SO question above (the question itself, not the answer), for the sendSmartBatteryCommand() function, I wrote some code that calls the function passing kSBInflowDisable as the selector (the variable which in the code).

Unlike the code using assertions, this one only works as root. If running as a regular user, IOServiceOpen() returns, weirdly enough, kIOReturnBadArgument (not kIOReturnNotPrivileged, as I would have expected). Perhaps this has to do with the initWithTask() method above.

The question

I need to perform a call with a different selector to this same Smart Battery Manager kext. Even so, I can't even get to the IOConnectCallMethod() since IOServiceOpen() fails, presumably because the initWithTask() method prevents any non-root users from opening the service.

The question, therefore, is this: how is IOPMAssertionCreateWithName() capable of creating a DisableInflow assertion without root privileges?

The only possibility I can think of is if there's a root-owned process to which requests are forwarded, and which performs the actual work of calling IOServiceOpen() and later IOConnectCallMethod() as root.

However, I'm hoping there's a different way of calling the Smart Battery Manager kext which doesn't require root (one that doesn't involve the IOServiceOpen() call.) Using IOPMAssertionCreateWithName() itself is not possible in my application, since I need to call a different selector within that kext, not the one that disables inflow.

It's also possible this is in fact a security vulnerability, which Apple will now fix in a future release as soon as it is alerted to this question. That would be too bad, but understandable.

Although running as root is a possibility in macOS, it's obviously desirable to avoid privilege elevation unless absolutely necessary. Also, in the future I'd like to run the same code under iOS, where it's impossible to run anything as root, in my understanding (note this is an app I'm developing for my own personal use; I understand linking to IOKit wipes out any chance of getting the app published in the App Store).

Paulw11
  • 108,386
  • 14
  • 159
  • 186
swineone
  • 2,296
  • 1
  • 18
  • 32
  • 1
    1) Although the comments say root, the code seems to be doing a different check for a specific admin privilege. 2) The code for `IOPMAssertionCreateWithName()` is also open source in the [IOKitUser package](https://opensource.apple.com/tarballs/IOKitUser/IOKitUser-1483.220.15.tar.gz). This shows that it is indeed communicating with a separate server process/daemon (`powerd`). – Ken Thomases Jan 14 '19 at 02:05
  • I'm now looking into [this](https://opensource.apple.com/source/PowerManagement/PowerManagement-733.221.1/pmconfigd/PMAssertions.c.auto.html) and I now believe `powerd` may be the mechanism through which a non-root process is able to create this assertion. I will update this later if I convince myself this is really it. – swineone Jan 14 '19 at 02:06
  • 1
    @KenThomases you appear to have nailed the issue. This would mean, then, that it's impossible to do what I want -- I must go through `IOServiceOpen()` since I need a difference selector, and for that I need root. Am I right? Too bad in that case. – swineone Jan 14 '19 at 02:14
  • My point 1 was that you may not need root, per se. Some sort of admin privilege, which may be controlled by a configuration option, may suffice. Also, the admin check is within `secureInflowDisable()`. If you're using a different selector, then a different function will be called, and whether it does a similar check or not is an open question. And I don't know if a kext can directly call a function defined in a different kext. If it can, you may not need to open a connection at all. – Ken Thomases Jan 14 '19 at 02:27
  • @KenThomases checking for `kIOClientPrivilegeAdministrator` means "is this task running as the root user?" At least that's what I understand the line `if (0 != token.val[0])` in `IOUserClient::clientHasPrivilege()` to mean, and this seems to be what it evaluates to in practice. You'll need to invoke it from a privileged helper tool unless you can find another way to run that code. – pmdj Jan 14 '19 at 10:34
  • @KenThomases regardless of redundant checks performed by the selectors themselves, `initWithTask()` already checks for root. This is why I'm getting stuck at `IOServiceOpen()`, even before I can pass in a selector (which would be done in `IOConnectCallMethod()`. It's blocking me before it even knows what I'm going to call. Now, the part I didn't get was about a kext calling a function in a different kext -- I'm not trying to write a kext here, quite the contrary: I want to write userland code that runs without root privileges. – swineone Jan 14 '19 at 12:10
  • Ah, I misunderstood what you were trying to do. Is there a reason you haven't said what selector you're actually trying to invoke? Presumably, it's there for a reason. So, some system software invokes it. Perhaps you can persuade that to invoke it on behalf the same way that `IOPMAssertionCreateWithName()` communicates with `powerd` to invoke the `kSBInflowDisable` selector for you. – Ken Thomases Jan 14 '19 at 17:05
  • @KenThomases I have further analyzed Apple's [power management package's sources](https://opensource.apple.com/source/PowerManagement/PowerManagement-733.221.1/). The function `sendSmartBatteryCommand()`, defined in both `pmconfigd/PMAssertions.c` (related to `powerd`) and `pmtool/pmtool.c`, communicates with the kext when the assertion is created by `IOPMAssertionCreateWithName()`. However, only `powerd` is of interest here since `pmtool` is not a daemon, and it only calls two selectors (`InflowDisable` and `InhibitCharging`). This appears to be a dead-end, unfortunately. – swineone Jan 14 '19 at 19:01

0 Answers0