Connecting to user dbus as root

Question:

If we open a python interpreter normally and enter the following:

import dbus
bus = dbus.SessionBus()
bus.list_names()

We see all the services on the user’s session dbus. Now suppose we wanted to do some root-only things in the same script to determine information to pass through dbus, so we run the interpreter with sudo python and run the same thing, we only see a short list of items on the root user’s session dbus, and attempting to connect to anything that was on the user dbus with get_object produces a not found error accordingly.

So far I’ve tried inserting

import os

os.seteuid(int(os.environ['SUDO_UID']))

But this only makes SessionBus() give a org.freedesktop.DBus.Error.NoReply so this is probably nonsense. Is there a way to connect to a user’s dbus service as a super user, with the python dbus bindings?

Asked By: day

||

Answers:

You can set DBUS_SESSION_BUS_ADDRESS environment variable to choose the dbus session you want to connect to.

Incorrect permissions (i.e., missing the seteuid) causes an immediate NoReply, and not defining DBUS_SESSION_BUS_ADDRESS responded with Using X11 for dbus-daemon autolaunch was disabled at compile time, set your DBUS_SESSION_BUS_ADDRESS instead.

Here’s the test code I used:

import os
import dbus

# become user
uid = os.environ["SUDO_UID"]
print(f"I'm {os.geteuid()}, becoming {uid}")
os.seteuid(int(uid))

# set the dbus address
os.environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path=/run/user/{uid}/bus"

bus = dbus.SessionBus()
print(bus.list_names())

# I'm 0, becoming 1000
# dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String('org.fr .. <snip>
Answered By: teprrr

I have little knowledge about DBus, but that question got me curious.

TL;DR: Use dbus.bus.BusConnection with the socket address for the target user and seteuid for gaining access.

First question: What socket does DBus connect to for the session bus?

$ cat list_bus.py 
import dbus
print(dbus.SessionBus().list_names())
$ strace -o list_bus.trace python3 list_bus.py
$ grep ^connect list_bus.trace 
connect(3, {sa_family=AF_UNIX, sun_path="/run/user/1000/bus"}, 20) = 0

Maybe it relies on environment variables for this? Gotcha!

$ env|grep /run/user/1000/bus
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

Stracing the behaviour from the root account it appears that it does not know the address to connect to. Googling for the variable name got me to the D-Bus Specification, section "Well-known Message Bus Instances".

Second question: Can we connect directly to the socket without having the D-Bus library guess the right address? The dbus-python tutorial states:

For special purposes, you might use a non-default Bus, or a connection which isn’t a Bus at all, using some new API added in dbus-python 0.81.0.

Looking at the changelog, this appears to refer to these:

Bus has a superclass dbus.bus.BusConnection (a connection to a bus daemon, but without the shared-connection semantics or any deprecated API) for the benefit of those wanting to subclass bus daemon connections

Let’s try this out:

$ python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
145

How about root access?

# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3/dist-packages/dbus/bus.py", line 124, in __new__
    bus = cls._new_for_bus(address_or_type, mainloop=mainloop)
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.NoReply: Did not
  receive a reply. Possible causes include: the remote application did not send
  a reply, the message bus security policy blocked the reply, the reply timeout
  expired, or the network connection was broken.
>>> import os
>>> os.seteuid(1000)
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143

So this answers the question: Use BusConnection instead of SessionBus and specify the address explicitly, combined with seteuid to gain access.

Bonus: Connect as root without seteuid

Still I’d like to know if it is possible
to access the bus directly as root user, without resorting to seteuid. After
a few search queries, I found a systemd ticket
with this remark:

dbus-daemon is the component enforcing access … (but you can drop an xml policy file in, to make it so).

This led me to an askubuntu question discussing how to modify the site local session bus policy.

Just to play with it, I ran this in one terminal:

$ cp /usr/share/dbus-1/session.conf session.conf
$ (edit session.conf to modify the include for local customization)
$ diff /usr/share/dbus-1/session.conf session.conf
50c50
<   <include ignore_missing="yes">/etc/dbus-1/session-local.conf</include>
---
>   <include ignore_missing="yes">session-local.conf</include>
$ cat > session-local.conf
<busconfig>
  <policy context="mandatory">
    <allow user="root"/>
  </policy>
</busconfig>
$ dbus-daemon --config-file session.conf --print-address
unix_abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98
$ dbus-daemon --config-file session.conf --print-address
unix_abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98

In another terminal, I can not attach to this bus as a root user:

# python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
>>> from dbus.bus import BusConnection
>>> address = "unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98"
>>> BusConnection(address).list_names()
dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String(':1.0')], signature=dbus.Signature('s'))

This should also enable accessing all session busses on the system, when installing session-local.conf globally:

# cp session-local.conf /etc/dbus-1/session-local.conf
# kill -HUP 1865   # reload config of my users session dbus-daemon
# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143

And it works – now root can connect to any session bus without resorting to seteuid. Don’t forget to

# rm /etc/dbus-1/session-local.conf 

if your root user does not need this power.

Answered By: Bluehorn

Bluehorn’s answer helped me. I figured I’d share my solution. I’m only a few months into learning Python (coming from just shell scripting) so if this is really wrong and just happens to work on my system please let me know.

These are parts from a daemon I wrote to monitor CPU temps and control the fan speeds in Linux so it needs root permissions. Not sure how well it would work if ran as a regular user when multiple users are logged in. I’m guessing it wouldn’t…


import os, pwd
from dbus import SessionBus, Interface
from dbus.bus import BusConnection

# Subclassing dbus.Interface because why not

class Dbus(Interface):
    def __init__(self, uid):
        method = 'org.freedesktop.Notifications'
        path = '/' + method.replace('.', '/')
        if os.getuid() == uid:
            obj = SessionBus().get_object( method, path )
        else:
            os.seteuid(uid)
            obj = BusConnection( "unix:path=/run/user/" + str(uid) + "/bus" )
            obj.get_object( method, path )

        super().__init__(obj, method)

# Did this so my notifications would work
# when running as root or non root

class DbusNotificationHandler:

    app_icon = r"/path/to/my/apps/icon.png"
    name     = "MacFanD"

    def __init__(self):
        loggedIn, users = [ os.getlogin() ], []
        for login in pwd.getpwall():
            if login.pw_name in loggedIn:
                users.append( login.pw_uid )

        self.users = []
        for i in users:
            self.users.append( Dbus(i) )

    def notification(self, msg, mtype=None):
        if not isinstance(msg, list) or len(msg) < 2:
            raise TypeError("Expecting a list of 2 for 'msg' parameter")

        hint = {}

        if mtype == 'temp':
            icon = 'dialog-warning'
            hint['urgency'] = 2
            db_id = 498237618
            timeout = 0
        elif mtype == 'warn':
            icon = 'dialog-warning'
            hint['urgency'] = 2
            db_id = 0
            timeout = 5000
        else:
            icon = self.app_icon
            hint['urgency'] = 1
            db_id = 0
            timeout = 5000

        for db in self.users:
            db.Notify( self.name, db_id, icon, msg[0], msg[1:], [], hint, timeout )

handler = DbusNotificationHandler()
notify = handler.notification

msg = [ "Daemon Started", "Daemon is now running - %s"%os.getpid() ]
notify(msg)

temp = "95 Celsius"
msg = [ "High Temp Warning", "CPU temperature has reached %s"%temp ]
notify(msg, 'warn')
Answered By: Erik Beebe
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.