Can't connect to system PolicyKit1.Authority via DBusProxy

Hi there! I’m currently facing an issue trying to communicate with the “org.freedesktop.PolicyKit1.Authority” system service through a DBusProxy on my application. I have never interacted with DBus before, but I believe to be in the right path for what I actually need to do: prompt the user for elevating privileges (typing in their “sudo” password on a modal) and granting the application permissions to execute commands for interacting with dkms in the background.

Here is the current code in my application.ts file:

import Adw from "gi://Adw";
import Gio from "gi://Gio";
import GLib from "gi://GLib";
import GObject from "gi://GObject";
import Gst from "gi://Gst";
import Gtk from "gi://Gtk?version=4.0";

import { Window } from "./window.js";

const orgFreedesktopPolicyKit1AuthorityI = `
<node>
    <interface name="org.freedesktop.PolicyKit1.Authority">
        <method name="EnumerateActions">
        <arg type="s" direction="in" name="locale"/>
        <arg type="a(ssssssuuua{ss})" direction="out" name="action_descriptions"/>
        </method>
    </interface>
</node>`;

export class Application extends Adw.Application {
    private window?: Window;

    static {
        GObject.registerClass(this);
    }

    constructor() {
        super({
            application_id: pkg.name,
            resource_base_path: "/app/tarcisiosurdi/XPDA/",
            flags: Gio.ApplicationFlags.DEFAULT_FLAGS,
        });
        GLib.set_application_name(_("Xbox Peripherals Driver Assistant"));
        GLib.setenv("PULSE_PROP_media.role", "production", true);
        GLib.setenv("PULSE_PROP_application.icon_name", pkg.name, true);
    }

    private initAppMenu(): void {
        const aboutAction = new Gio.SimpleAction({ name: "about" });
        aboutAction.connect("activate", this.showAbout.bind(this));
        this.add_action(aboutAction);

        const quitAction = new Gio.SimpleAction({ name: "quit" });
        quitAction.connect("activate", () => {
            if (this.window) {
                this.window.close();
            }
        });
        this.add_action(quitAction);

        this.set_accels_for_action("app.quit", ["<Primary>q"]);
        this.set_accels_for_action("win.show-help-overlay", [
            "<Primary>question",
        ]);
    }

    public vfunc_startup(): void {
        super.vfunc_startup();
        log("XPDA (%s)".format(pkg.name));
        log("Version: %s".format(pkg.version));

        Gst.init(null);

        this.initAppMenu();
        this.enumerateActions().catch((e) => log(e));
    }

    public vfunc_activate(): void {
        if (!this.window) {
            this.window = new Window({ application: this });
            if (pkg.name.endsWith("Devel")) this.window.add_css_class("devel");
        }
        this.window.present();
    }

    private showAbout(): void {
        let appName = GLib.get_application_name();
        if (!appName) appName = _("XPDA");

        const aboutDialog = new Adw.AboutDialog({
            artists: [],
            developers: ["Tarcísio Surdi <tarcisio.surdi@pm.me>"],
            /* Translators: Replace "translator-credits" with your names, one name per line */
            translator_credits: _("translator-credits"),
            application_name: appName,
            license_type: Gtk.License.AGPL_3_0,
            application_icon: pkg.name,
            version: pkg.version,
            website: "https://github.com/TarsiSurdi/xpda",
            issue_url: "https://github.com/TarsiSurdi/xpda/issues",
            copyright: "Copyright 2024 Tarcísio Surdi",
        });

        aboutDialog.present(this.window);
    }

    private async enumerateActions() {
        const AuthProxy = Gio.DBusProxy.makeProxyWrapper(
            orgFreedesktopPolicyKit1AuthorityI
        );

        let proxy = null;

        new AuthProxy(
            Gio.DBus.system,
            "org.freedesktop.PolicyKit1.Authority",
            "org/freedesktop/PolicyKit1/Authority",
            (sourceObj: Gio.DBusProxy, error: Gio.DBusError) => {
                // If @error is not `null` it will be an Error object indicating the
                // failure. @proxy will be `null` in this case.
                if (error !== null) {
                    log(error);
                    return;
                }

                // At this point the proxy is initialized and you can start calling
                // functions, using properties and so on.
                proxy = sourceObj;
                proxy.call(
                    "EnumerateActions",
                    new GLib.Variant("s", ""),
                    Gio.DBusCallFlags.NONE,
                    -1,
                    null
                );
            },
            // Optional Gio.Cancellable object. Pass `null` if you need to pass flags.
            null,
            // Optional flags passed to the Gio.DBusProxy constructor
            Gio.DBusProxyFlags.NONE
        );
    }
}

When trying this code out, I get the following error: JS LOG: Gio.IOErrorEnum: Could not connect: No such file or directory. What am I doing wrong here? I believe the issue is around the values I’m providing as bus_name, object_path or interface_name because of the “404” error, but I can’t determine which of these arguments is wrong…

Also, I’m calling EnumerateActions just because it seems to be a simple method for debugging my app’s communication with DBus. I believe once I have established a successful connection that I would have to use the CheckAuthorization method and try to execute things with pkexec for what I need to do? Are there any examples out there trying to do what I need?

For example, I can call this method through the following command on my system:

sudo gdbus call --system \
--dest org.freedesktop.PolicyKit1 \
--object-path /org/freedesktop/PolicyKit1/Authority \
--method org.freedesktop.PolicyKit1.Authority.EnumerateActions ""

I’m essentially trying to the the same on the app…

Shouldn’t the bus_name argument be org.freedesktop.PolicyKit1, rather than org.freedesktop.PolicyKit1.Authority?

I think you’re correct about that, but the same error persists…

** (process:53653): WARNING **: 16:06:49.367: Error writing credentials to socket: Error sending message: Broken pipe
Gjs-Message: 16:06:49.578: JS LOG: XPDA (app.tarcisiosurdi.XPDA.Devel)
Gjs-Message: 16:06:49.579: JS LOG: Version: 1.0.0-a712515
Gjs-Message: 16:06:49.708: JS LOG: Gio.IOErrorEnum: Could not connect: No such file or directory

Also I found it strange that this “broken pipe” thing only seems to trigger sometimes when I run the application.

Well, for what it’s worth, the most common way for extensions to do this is using Gio.Subprocess to launch pkexec $cmd. There shouldn’t be any D-Bus interaction required for that to work; is that an easier solve than debugging this?

The only request we make is that hard-coded commands (not commands configured by a user), be in an system directory or otherwise not-writable by users. This is to prevent something replacing e.g. ~/.local/bin/foobar with another malicious script, which the extension then unknowingly executes.

I’m under the impression that in order for the application to call methods on a system bus it first needs to acquire privileges to do so, as executing

sudo gdbus call --system \
--dest org.freedesktop.PolicyKit1 \
--object-path /org/freedesktop/PolicyKit1/Authority \
--method org.freedesktop.PolicyKit1.Authority.EnumerateActions ""

without sudo doesn’t work. Could that be related to why the application can’t access the method?

How do you even acquire elevated privileges? I’ve read somewhere that it has to be triggered by user action?

Ohhh, I didn’t know that, thank you!

I will be trying it out and testing if it’s able to solve my use cases!

1 Like

Many interfaces on the system bus are locked down pretty tight, but I’m not terribly familiar with what PolicyKit does here.

polkit is used by an already-privileged process (typically a system daemon running with elevated privileges) to decide whether a request from a lower-privileged process should be authorised.

You don’t call polkit over D-Bus to gain elevated privileges for your app. A daemon which your app is interacting with calls polkit to decide whether to accept your app’s request.

So in the case of using pkexec, what’s actually happening is that pkexec is a setuid root binary, which has elevated privileges once you launch it. It talks to polkit over D-Bus, which decides whether the requested subcommand should be authorised, and then, if so, pkexec uses its elevated privileges to do the privileged thing.

Using pkexec is quite hacky, because the polkit actions for it have to be quite vague — they amount to asking the user if they want to allow your app to run some complicated-looking subprocess. Architecturally a better solution is for DKMS to provide an interface to unprivileged processes. Your app would call this interface, with a request to do a specific action, DKMS would talk to polkit to see if that specific action is authorised, and then do the action (or refuse to if unauthorised). The difference is the specificity of the authorisation requests to the user. It’s easier for a user to make a decision about (say) “sign a kernel module” rather than “run dkms --sign /some/complex/path --other --indecipherable --args.

Apologies if I’ve misinterpreted your use case. Hopefully the general concepts make sense.

You might find the polkit architecture reference useful. It has diagrams.

2 Likes

This is how I thought it would work in the first place, then it became my objective to talk to Polkit directly instead and ask for permission… you’ve interpreted me perfectly!

Thank you for the explanations, I now understand all of this a little bit better and why things work the way they do on the platform! I must confess that I had never even read GTK application code before starting this project and I thought that asking LLMs for how to do these things is how I got in this situation in the first place instead of RTFM / documentation, sorry about that.

So… I was testing this and I’m now able to run simple command such as ls -l / successfully! The problem is, the moment I try to call pkexec directly I get this error:

Gjs-Console-CRITICAL **: 09:05:14.581: GLib.SpawnError: Failed to execute child process “pkexec” (No such file or directory)

After this I tried running which pkexec but got this:

Gjs-Console-CRITICAL **: 09:07:24.273: Error: which: no pkexec in (/app/bin:/usr/bin)

This is weird because running pkexec dkms status through the CLI works just fine… is this a limitation of the Flatpak sandbox? Do I have to define where pkexec is in a GResource?

The run time does not include pkexec, and even if it did, it would not be able to run on your system.

No, because of course where it “would” be is in your host OS, which is not available in the sandbox.

Hmmm, so how exactly can I call it? Do I need to define something else on my flatpak manifest?

You don’t.

Using pkexec is typically a mistake on anything that isn’t an interactive shell. Consider it like “sudo”, which it basically is.

If you are trying to contact a privileged user service on the system bus, you should do it programmatically.

Please, avoid LLMs, and instead explain what are you trying to achieve, with enough context to avoid going back and forth for 20 replies.

Ok. I’m trying to call dkms and it’s subcommands such as status, remove, install in order to develop an app for managing kernel modules.

I would like for the app to run dkms status on startup, parse the information provided in it’s output and show the user available options for managing those modules.

I started this out thinking that I would have to communicate with DBus to do this, then I moved to using Gio.Subprocess but the commands it gives me access to are rather limited. How do I prompt the user for permission and execute these “privileged” commands?

I’m just looking for a way to do this here and coming up with ways in which I think I could make this work, but having never developed anything like this before I’m running into some dead ends, as expected.

I regularly make use of apps that request for my password in order to elevate their privileges and run / access stuff such as input-remapper, btrfs-assistant, GtkStressTesting…

You cannot do this from a Flatpak.

In order to implement this you will need two separate components:

  • a system service that runs as a privileged user, and operates on DKMS; this service will expose a D-Bus API on the system bus, and will use PolKit to allow for privilege escalation
  • an application that talks to the system service through the system bus; the application can be distributed as a Flatpak, but one that will have to explicitly add a sandbox exception for talking to the specific interface on the system bus, otherwise you’ll also need to define and implement a desktop portal that will proxy the system bus to the session bus through a trusted middle layer

In practice, the first part will have to be packaged by your distribution and installed on the host system; the second part can be packaged via Flatpak, though it does have a sandbox exception.

None of those are using Flatpak or a sandbox; and if they do, they also go through a system service and/or a portal that is not part of the sandbox.

1 Like

Looks like this will be way harder than I initially expected…

Do you happen to know of any resources on how to develop such a system service for DBus communication?

Yes. Writing applications that talk to system services is not exactly trivial; it’s definitely harder when you have to write the system service in the first place: writing a high privileged component requires careful consideration of many aspects of the platform—security, permissions, privilege escalation, separation of contexts, safe programming practices. It’s not something I’d recommend doing if you’re not already somewhat well-versed with programming, and you’re not already familiar with the platform itself.

Reading a book or a tutorial won’t help you.

I’d recommend getting familiar with the existing system services already on your machine; look at all the services exposed on the system bus—you can check using a D-Bus inspector like D-Spy; find their source code repository, and start reading their implementation. Once you understand how they are put together, you can attempt at following their patterns.