D-Bus messages, brokers, and sd-bus

Trace bus messages, policy decisions and connection lifetimes.

14 min readChapter 103 of 168Intermediate → advanced
In this chapter

Before you start#

Review userspace boundaries, Unix sockets, file descriptor lifetime, and threads and event loops. The source walkthrough uses systemd v256 sd-bus and a separately inspected D-Bus 1.14.10 daemon archive. The versions are independent reading examples, not a tested distribution combination.

Learning goals#

Describe the structure and routing of a D-Bus method call and reply, identify ownership of a bus name, message, callback data and passed descriptor, and distinguish a bus delivery decision from an application's authorization decision. Explain what a private session bus test establishes and what it cannot prove about a system bus.

Source walkthrough#

A routed method and correlated reply
A routed method and correlated reply1. Client to Message bus: method call, serial 41. 2. Message bus to Message bus: check delivery policy and name owner. 3. Message bus to Service: route to current owner. 4. Service to Service: dispatch callback and validate. 5. Service to Message bus (return): reply with reply serial 41. 6. Message bus to Client (return): deliver reply.ClientMessage busServicemethod call, serial 41check deliverypolicy and nameownerroute to current ownerdispatchcallback andvalidatereply with reply serial41deliver reply
  1. ClientMessage bus

    method call, serial 41

  2. Message busMessage bus

    check delivery policy and name owner

  3. Message busService

    route to current owner

  4. ServiceService

    dispatch callback and validate

  5. ServiceMessage bus

    Return. reply with reply serial 41

  6. Message busClient

    Return. deliver reply

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Client → Message bus: method call, serial 41
  2. Message bus → Message bus: check delivery policy and name owner
  3. Message bus → Service: route to current owner
  4. Service → Service: dispatch callback and validate
  5. Service → Message bus (return): reply with reply serial 41
  6. Message bus → Client (return): deliver reply
Conceptual model. A synthetic message exchange, not a captured bus trace; policy and disconnects can stop delivery.

D-Bus is a message protocol and a collection of interfaces used by many userspace services. A bus is a routing and policy participant, commonly reachable over a Unix socket; clients are independent processes, and each has its own address space and file descriptors. A client can also use a direct peer connection in supported arrangements, so not every D-Bus connection necessarily traverses a bus daemon. dbus-daemon, dbus-broker and client libraries are different implementations or components. An application using sd-bus is not automatically speaking directly to the kernel, and the kernel does not interpret D-Bus method names.

A method call typically contains a destination, object path, interface, member, serial number, signature and typed body. The destination identifies the intended service; the object path identifies an object within that service, while interface and member identify an operation. The signature describes the types in the body. The receiver must still validate values, credentials, state and size. A bus unique name identifies a connection during its lifetime, whereas a well-known name is an owned, potentially queued service name. Ownership can change after disconnect. A process must not treat a cached unique owner as a permanent identity of a service.

The following exact excerpt is from systemd v256 src/libsystemd/sd-bus/bus-objects.c, function bus_process_object; the file is LGPL-2.1-or-later. The surrounding assertions and branches are omitted:

c
        if (bus->is_monitor)
                return 0;

        if (m->header->type != SD_BUS_MESSAGE_METHOD_CALL)
                return 0;

A monitor connection is not handled here as a service object dispatcher. Next, the message's header type is checked; a non-method message returns without taking this object-call path. The function later checks whether object nodes exist, whether a bus-client call has a destination, and whether path and member are present. Those checks are specific to this implementation and path. A returned zero here means this function did not handle a message; the event loop and caller decide what to do next. It is not a general denial or proof that a peer received an error reply.

The next exact excerpt is from v256 src/libsystemd/sd-bus/sd-bus.c, process_message, also LGPL-2.1-or-later. The preceding dispatch stages and following function are omitted:

c
        r = bus_process_object(bus, m);

finish:
        bus->current_message = NULL;
        return r;

The call passes the bus and current message to object processing after earlier stages such as filters and match processing have had a chance to handle it. The common finish label clears the bus's current-message pointer and returns the result. Clearing that pointer is part of dispatch state cleanup; it does not give application code permission to retain a borrowed message pointer indefinitely. If a callback needs a message after its allowed lifetime, it must obtain and later release an appropriate reference under the documented API. The function's return and the bus event-loop contract determine whether more work is pending or an error occurred.

Message framing and a worked exchange#

D-Bus messages use a defined binary format with an endianness marker, message type, flags, protocol version, body length, serial and an array of header fields. Types have alignment requirements, and padding bytes are not application values. A header may contain fields such as path, interface, member, destination, reply serial, signature and UNIX_FDS count; which are required depends on message type and protocol rules. The encoded body must match the declared signature. A receiver must validate lengths, alignment, string termination and type nesting rather than treating a peer's bytes as an in-process structure.

Fixed header offsets for a teaching call

Synthetic teaching data, not a device capture.

Select a field to connect its position, bytes, and meaning.

16 bytes in file order. Offsets start at zero.

Selected: Endianness. 'l' marks little-endian encoding..

Read the byte table
OffsetFieldHex bytesInterpretation
0Endianness6c'l' marks little-endian encoding.
1Message type01Value 1 is SD_BUS_MESSAGE_METHOD_CALL.
2Flags00No flag bits are set, so a reply is expected.
3Protocol version01message_from_header accepts versions 1 and 2.
4Body length04 00 00 00One uint32 argument, 4 bytes, little-endian.
8Serial29 00 00 00Connection-scoped serial 41 in this example, not a global identifier.
12Header-field length20 00 00 00Example length 32, not computed; the body starts after the 8-byte-aligned field array.
Synthetic teaching data. Offsets come from struct bus_header in the v256 sd-bus source. The byte values are a synthetic header for the Add(7) example; the header-field length is an arbitrary example, not a computed encoding.

The offsets in that visual come from struct bus_header in the v256 sd-bus header bus-protocol.h, which carries an LGPL-2.1-or-later notice. The byte values are a synthetic header for the Add(7) example below. The header-field length is an arbitrary example, not a computed encoding, so the body offset shown in the table would change with a real field array. The message type value 1 is SD_BUS_MESSAGE_METHOD_CALL.

In the v256 sd-bus source src/libsystemd/sd-bus/bus-message.c, message_from_header checks a received header before it builds a message object. The file is LGPL-2.1-or-later. Lines 350-361 are quoted exactly; the later length and field-array checks are omitted:

c
        if (message_size < sizeof(struct bus_header))
                return -EBADMSG;

        h = buffer;
        if (!IN_SET(h->version, 1, 2))
                return -EBADMSG;

        if (h->type == _SD_BUS_MESSAGE_TYPE_INVALID)
                return -EBADMSG;

        if (!IN_SET(h->endian, BUS_LITTLE_ENDIAN, BUS_BIG_ENDIAN))
                return -EBADMSG;

These checks cover the minimum header size, the protocol version (1 or 2 in this source), the message type and the endianness marker. They do not validate every field or the body contents. Later lines in the same function check that the field array and body length agree with the message size. A header that passes is still a peer's message, so the receiver's own argument and authorization checks still apply.

Consider a synthetic method call from connection :1.8 to well-known name org.example.Counter, object /org/example/Counter, interface org.example.Counter, member Add. Let its sender serial be 41 and body signature be u, an unsigned 32-bit integer, with value 7. The serial identifies this outgoing message within the sender's connection; it is not a global timestamp. The receiver replies with a method-return message whose reply-serial is 41 and a body signature u, value 12. The client associates the response with its pending request by the reply serial and connection context. A missing reply, an error message and a successful method return are distinct results.

For a deliberately small alignment example, if a body begins at an offset already divisible by four, an unsigned 32-bit value 7 occupies four bytes in the message's declared byte order: in little endian those body bytes are 07 00 00 00. This illustrates one value only, not a complete encoded D-Bus message. A string, array or variant has additional length, signature and alignment rules. A variant carries a contained type and value; the receiver cannot safely interpret it as a fixed C structure without checking that type. GLib's GVariant format is a related typed serialization system, but a GVariant memory image is not automatically a D-Bus wire message.

A Unix file descriptor can be transferred out of band with SCM_RIGHTS on a supporting Unix transport. The D-Bus message body carries a handle index into its attached descriptor array, and the UNIX_FDS header indicates the count. Suppose a client attaches one readable descriptor and the body handle is index 0. Sending it does not make the integer value meaningful in the receiving process; the receiver obtains its own descriptor reference and number. Each side must close its own reference at the appropriate time. A rejected message, failed send or disconnected peer requires cleanup under the client library's ownership contract. Do not assume every transport supports descriptor passing or that a descriptor received from a peer is trustworthy.

Routing, names and policy#

On a message bus, a connection authenticates under the transport and bus configuration. The EXTERNAL mechanism can use credentials supplied by the operating system on a local transport; it is not an application password protocol and does not itself authorize every method. After the protocol handshake and Hello, the bus assigns a unique connection name. A service can request a well-known name, and the bus may grant it, queue the requester or refuse it under policy and flags. A client can watch owner changes, but a name-owner change is a race boundary: the destination that owned a name earlier may have disconnected before the next method call.

A broker routes a directed method call toward the current owner if the message and policy permit delivery. A method call can yield a normal reply, an error, a timeout or a disconnect. Signals are one-way messages and match rules influence which interested connections receive them; a signal is not automatically an acknowledgment that every listener processed a state change. Activation may ask a configured mechanism to start a service when a name is not currently owned, if policy and configuration allow it. A private bus with no service definition may instead return a service-unknown error. Never assume that all sessions use the same broker, activation configuration, or authorization policy.

The D-Bus 1.14.10 release's bus/dispatch.c, inspected from the official release archive and matched to its tagged Git file, invokes bus_context_check_security_policy while dispatching and has error paths before bus_dispatch_matches. The archive file carries an Academic Free License 2.1 and GPL-2-or-later dual notice. This is a bounded source observation about that daemon release, not a claim that dbus-broker uses its internal objects or the same call chain. The public D-Bus specification defines interoperable protocol concepts; implementations choose their own internal queues, connection objects and event loops.

Dispatch decisions in the 1.14.10 daemon
  1. Capture

    The message is captured into the transaction before any delivery side effect.

  2. Bus driver target

    A message to org.freedesktop.DBus passes the security policy check, then the bus driver handles it.

  3. Unregistered sender

    A connection that is not yet active is disconnected; clients must talk to the bus driver first.

  4. Named destination

    With no owner, activation may start a service if auto-start is allowed; otherwise the call gets NameHasNoOwner.

  5. Recipient policy

    bus_dispatch_matches applies the security policy to the addressed recipient before delivery.

  6. Match rules

    Other connections receive the message only when their match rules accept it.

Source map. A simplified reading of bus_dispatch and bus_dispatch_matches in D-Bus 1.14.10; not a model of dbus-broker and not a captured bus trace.

In the same 1.14.10 daemon release, bus/dispatch.c, the file carries a dual notice: Academic Free License 2.1 and the GNU GPL (version 2 or later). bus_dispatch_matches applies the security policy to an addressed recipient before it considers Unix file-descriptor support and delivery. Lines 145-152 are quoted exactly:

c
  /* First, send the message to the addressed_recipient, if there is one. */
  if (addressed_recipient != NULL)
    {
      if (!bus_context_check_security_policy (context, transaction,
                                              sender, addressed_recipient,
                                              addressed_recipient,
                                              message, NULL, error))
        return FALSE;

A FALSE return ends delivery on this path, and the policy check has filled in the error. This is one bus-side decision for a directed message in that daemon release. It does not show what every sender receives, what dbus-broker does, or whether the service would have accepted the call. Policy and service authorization are separate boundaries.

Bus policy controls who may connect, own names or send and receive certain messages under a configuration. A successful bus delivery does not prove that the application service authorized a privileged operation. A service can inspect credentials, validate arguments and consult an authorization service such as polkit before performing its operation. Conversely, bus policy may reject a call before the service sees it. These are separate trust boundaries, and their exact checks depend on the deployed configuration. Do not change host system bus policy or ask a real service to perform a privileged operation for this chapter.

sd-bus objects, slots and event loops#

In sd-bus, a service can register object vtables defining methods, properties and signals. A callback receives message and user data through the library interface. The vtable, user data and any associated resources must remain valid for the registration's lifetime according to the API contract. An sd_bus_slot represents a registration or pending operation whose reference and release rules govern when it is disconnected. If a service drops the slot or frees user data while a callback can still use it, the result can be a use-after-free. A reference to the slot does not automatically synchronize application data shared with worker threads.

A service event loop repeatedly processes pending bus work and waits for readiness or a timeout. Processing can invoke application callbacks, and those callbacks may send replies or arrange asynchronous work. Recursive invocation of sd_bus_process is restricted by the library's state checks. An asynchronous call owns or references a pending callback registration until completion, cancellation or disconnect under the API's rules. A reply may arrive after the application has requested shutdown; teardown must prevent new work, cancel or complete pending calls, release slots and references, and only then free associated user data. Do not assume that an event-loop callback is automatically safe to run concurrently with another arbitrary thread.

A client-side method call and server-side object dispatch are two separate processes. A service callback may return a D-Bus error for invalid arguments or authorization failure; a transport disconnect may prevent any reply from reaching the client. A timeout does not establish that the server did not perform the operation: the reply could be delayed or lost after the operation. Retrying a non-idempotent method can duplicate an action. To design safe recovery, define a request identifier or other application-level semantics rather than relying only on serial numbers, which are scoped to a connection.

For the synthetic Add(7) example, at T0 the client allocates a message and assigns serial 41. At T1 the bus checks and routes it; its own buffers and references have their own lifetime. At T2 the service dispatches to an object callback, validates the unsigned argument and changes its counter from 5 to 12 under the service's synchronization rule. At T3 it sends a return with reply serial 41. At T4 the client consumes the reply and releases its pending-call state. If the service disconnects at T2 before sending, the client cannot infer whether the counter changed. If a descriptor accompanied the call, each process closes its own received or original reference independently. This is a teaching timeline, not a captured bus trace.

Practice#

This optional lab creates a private session bus for one command and uses only the bus's own standard Hello-related name listing. It does not connect to or introspect the user's default system or session bus. The temporary daemon is managed by dbus-run-session and exits with the child. Use a terminal with dbus-run-session and dbus-send installed:

sh
dbus-run-session -- sh -c '
  dbus-send --session --print-reply --dest=org.freedesktop.DBus \
    /org/freedesktop/DBus org.freedesktop.DBus.ListNames
  if dbus-send --session --print-reply --dest=org.example.NoSuchService \
    /org/example/NoSuchService org.example.NoSuchService.Ping; then
    printf "%s\n" "unexpected service exists"
  else
    printf "%s\n" "missing service rejected"
  fi
'

The first reply should include the bus's own name and a unique name for the temporary client; exact unique numbers vary. The second call should fail because this private bus has no such service, producing an error and missing service rejected. An activation policy or local configuration can alter the error wording; record it without changing host configuration. If tools are missing or sandbox policy prevents a private bus, use the supplied serial-41 timeline and alignment example. The lab does not contact the host's default bus or capture private application messages. No bus or service configuration is written.

For a source-only deliverable, draw the client, bus and service with serial 41, reply serial 41, object path, signature and counter states. Add the two independent descriptor lifetimes and show where a policy rejection prevents callback delivery. State why the little-endian four-byte example is only a body fragment and why timeout does not prove that a method was not executed.

Knowledge check#

  1. How do unique and well-known names differ when a service disconnects?
  2. What joins the reply to the synthetic call, and why is the serial not a global transaction identifier?
  3. Why can a bus policy allow a message while the service still rejects the operation?
  4. What must happen to an sd-bus slot and its callback user data during shutdown?
  5. Why is a received file descriptor's integer not the sender's process-local descriptor identity?

Answers#

  1. The unique name belongs to a particular connection and ceases to identify that connection after disconnect. A well-known name can be acquired by another connection or have queued owners, so clients must account for ownership changes.
  2. The reply carries reply serial 41 on the relevant connection. Serial values are scoped to a connection and are not durable globally unique request IDs; reconnecting or retrying needs application-level semantics.
  3. The bus decides whether it will deliver under its configured policy. The service separately validates arguments, credentials and application authorization, potentially including polkit; either boundary can reject the request.
  4. Stop or cancel pending work and release the slot according to its API before freeing data that callbacks can access. A slot reference manages registration lifetime but does not serialize unrelated application state or automatically join worker threads.
  5. Descriptor numbers index separate process tables. Descriptor passing creates or transfers a reference visible under a receiver-local number; both processes have their own close obligations even when the underlying open-file object is shared.

Checkpoint#

Submit the serial and body-fragment diagram, a method/reply/error state table, and either the private-bus output or the supplied source-only analysis. Ready means naming both authorization boundaries, explaining slot and descriptor lifetimes, and identifying the uncertainty after a timeout. If unclear, retry with the four T0–T4 states and no descriptor before adding the out-of-band transfer.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.