Before you start#
Read D-Bus messages and desktop launchers. The source examples use xdg-desktop-portal 1.18.4 and AT-SPI 2.52.0 separately. No exercise contacts a real portal, accessibility bus, dialog, screen, or document.
Learning goals#
Follow an asynchronous portal request and its cleanup, explain the difference between a path and an open file descriptor as authority, and distinguish the portal service from the separate accessibility architecture. Reason about cancellation, response races and disappearing objects.
Source walkthrough#
- ClientPortal
Subscribe, then call with request token
- PortalBackend
Forward to selected backend
- BackendPortal
Return. Complete or cancel request
- PortalClient
Return. Response with status and permitted result
Time runs downward. Dashed arrows show returns.
Read the text version
- Client → Portal: Subscribe, then call with request token
- Portal → Backend: Forward to selected backend
- Backend → Portal (return): Complete or cancel request
- Portal → Client (return): Response with status and permitted result
A desktop portal is a service interface through which an application can ask for an operation mediated by desktop policy. Examples include selecting a file or requesting a screen-capture session. A frontend service exposes D-Bus interfaces and delegates desktop-specific work to one or more backend implementations. Backend selection depends on installed implementations, desktop configuration and the requested interface. A frontend being present does not prove that every backend or interface is available, nor that a particular user interaction will succeed. Portal mediation is operation-specific: a FileChooser result may make a selected file accessible through the Documents portal, but a returned URI or path is not a universal or durable host-filesystem grant. Access depends on the operation result, document-portal export, sandbox policy and the later open operation. See xdg-desktop-portal 1.18.4 request handling, file chooser, and the FileChooser interface.
The portal can be useful to sandboxed applications because the sandbox may deny direct access to a host resource while allowing a carefully scoped request to a broker. The broker identifies the caller under its available application identity and policy mechanisms, forwards a request, and returns only the result allowed by that interface. Application identity and permission storage differ with packaging and backend; an arbitrary string supplied by a client is not automatically trustworthy identity. An unsandboxed application can retain other access through its own operating-system permissions, so the existence of a portal is not a global Linux restriction on all applications.
Request objects and asynchronous replies
Many portal methods return a request object path rather than the final result. The client listens for a Response signal on that request. A handle token supplied in options must be a valid object-path element and becomes the final element of the request path; the path also includes the caller sender component. See the official Request interface. A careful client subscribes to the appropriate response before making the call, so an unusually fast response cannot arrive before its handler exists. It must still verify the actual returned handle, sender and interface as appropriate, and handle method errors. The token is an application-chosen correlation value, not a password or proof that a user approved access.
- ApplicationPortal frontend
method call with handle_token
- Portal frontendApplication
Return. returns request object path, not a decision
- BackendPortal frontend
backend outcome for this operation
- Portal frontendRequest object
ties the outcome to the request object
- Request objectApplication
Return. Response(code, results) signal
- ApplicationRequest object
Close() if the window goes away first
Time runs downward. Dashed arrows show returns.
Read the text version
- Application → Portal frontend: method call with handle_token
- Portal frontend → Application (return): returns request object path, not a decision
- Backend → Portal frontend: backend outcome for this operation
- Portal frontend → Request object: ties the outcome to the request object
- Request object → Application (return): Response(code, results) signal
- Application → Request object: Close() if the window goes away first
A request object is not a file descriptor. It is a D-Bus object with a lifetime and a close method. Calling close asks the service to terminate the pending request; a response, a cancellation and a disconnect can race. The client must use a state machine that accepts at most one terminal outcome for its own operation and releases subscriptions and temporary data even if the service vanishes. A successful method return containing a handle is not yet the user's decision. A response code and result dictionary communicate the final interface-specific outcome; cancellation and backend failure must not be interpreted as permission.
From xdg-desktop-portal 1.18.4 src/request.c, request_unexport, LGPL-2.0-or-later as stated in its file notice, with declarations and the earlier fd lookup omitted:
if (fd != -1)
close (fd);
request->exported = FALSE;
g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (request));
g_object_unref (request);
The conditional closes a stored descriptor if one exists. The next statement records that the object is no longer exported. Unexport removes the D-Bus interface from the connection, and the final statement drops a reference acquired for the exported request. The ordering is meaningful for this implementation's ownership, not a universal recipe for every portal or every descriptor. A callback holding another reference can still run; a raw request pointer kept after its last reference is dropped is invalid. The surrounding request_export code takes a reference and records export state, and has a warning path if interface export fails. The source's exact error handling should be evaluated in its version and context, not rewritten as an invented guarantee of successful export.
The export side of the same object is in src/request.c, xdg-desktop-portal 1.18.4, LGPL-2.0-or-later as stated in its notice (surrounding code omitted):
void
request_export (Request *request,
GDBusConnection *connection)
{
g_autoptr(GError) error = NULL;
if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (request),
connection,
request->id,
&error))
{
g_warning ("Error exporting request: %s", error->message);
g_clear_error (&error);
}
g_object_ref (request);
request->exported = TRUE;
}
Notice the order: the reference is taken and exported is set after the export call, and the code does not return early when g_dbus_interface_skeleton_export fails. The reference therefore pairs with the g_object_unref in request_unexport on both paths, as the excerpt shows. The excerpt does not show whether a request is usable after a failed export.
File chooser and document authority
A FileChooser request gives a desktop backend information needed to present a selection interface. The backend returns an outcome and selected locations according to the portal interface. A document portal can expose selected files into a sandbox through its own managed namespace and permission model, potentially using a FUSE-backed export. The resulting URI, exported path, open file descriptor and underlying file are different objects. A path names something that can change or become inaccessible; an open fd refers to an opened kernel file object with its own mode and lifetime. Passing an fd can grant access to that opened object, but not necessarily permission to open arbitrary neighboring paths.
A selection does not grant unlimited access to an entire host filesystem. The result's scope depends on the portal operation, permissions, document export and sandbox. A client must check response status and validate returned data; a cancelled chooser can return no usable file. Closing its own fd releases its reference, but other processes may still hold their own copies. Conversely, a path returned at one instant does not guarantee a later open will succeed: permissions can change, a file can be removed, or an export can expire. Resource cleanup must cover failures after receiving a successful response, such as failure to open or parse the selected file.
In xdg-desktop-portal 1.18.4 src/file-chooser.c, the send_response_in_thread_func implementation distinguishes a successful response from a nonzero one before processing its results. The following short excerpt is LGPL-2.0-or-later; intervening result handling is omitted:
response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response"));
options = (GVariant *)g_object_get_data (G_OBJECT (request), "options");
if (response != 0)
goto out;
The first statement retrieves the stored response code from the request object, and the second retrieves the options. The branch sends a nonzero outcome to cleanup before processing successful results; code must not read a successful file result merely because the request object exists. The source later emits a response and manages request lifetime. This single statement is not itself an access-control decision; it is an implementation step in an asynchronous result path, and its surrounding checks determine what results are returned.
A cancellation race
Consider a fictional application request with token lesson42. Before calling the portal, the application subscribes to the predicted request path for that token and records state pending. The method returns the actual request handle. The backend responds with cancellation code 1 while the application is also closing its window. If the response is delivered first, the callback atomically changes application state to finished-cancelled, removes its subscription and frees its request data. A subsequent local close action sees the terminal state and does not free the same data twice. If the window closes first, the application sends the request's close method and marks its own state as closing; a late response is handled or ignored under a single documented ownership rule. Disconnection is a third outcome and must also release local resources.
Synthetic teaching data, not a device capture.
- pending
Subscription and local request data exist; no response has been accepted.
- Response first
The first terminal event wins; cancellation code 1 is not approval.
- Window closes first
Close() is sent and local state moves to closing; a late Response is handled once or ignored.
- Disconnect
Loss of the peer releases local data and subscriptions as a third terminal route.
Now suppose a different request returns success with an allowed file fd. The receiving process owns its received descriptor according to the binding's transfer rules. It can read that opened file subject to its mode, but it cannot infer that the whole parent directory is accessible. If the read fails or parsing is rejected, it closes the fd and destroys its result object. If a worker thread consumes the fd, the application must give that worker a well-defined reference or duplicate and avoid closing the same numeric fd while another thread still relies on it. Fd numbers can be reused, so a stale integer is not a durable identity for the original open file.
Screen-capture sessions
A ScreenCast flow is usually longer than a single chooser request. Conceptually a client creates a portal session, asks to select sources, starts the session, and obtains a PipeWire remote connection through the interface when authorized. These are separate asynchronous operations with handles, responses and failure paths. A session object persists across requests and has its own close and lifecycle notifications. A screen source is subject to user and backend policy; the sequence does not grant a client the right to capture silently. The exact interface version, source types, permission persistence and backend behavior must be checked for a real deployment.
The returned PipeWire remote is an fd-backed connection capability under the interface's rules, not a screenshot. A stream may subsequently carry frames and metadata, and its buffers have their own ownership and timing. Permission to create a session does not prove that a frame arrived, that the source still exists, or that a particular pixel format is supported. Disconnect, a user revocation or backend shutdown can end the session while the client has pending work. Clients need to stop consumers, close descriptors and release request/session references without treating a late callback as an active session. This chapter does not open a remote or capture content.
Accessibility is a separate service boundary
AT-SPI exposes accessibility objects, interfaces, roles, states and actions so assistive technologies can navigate and operate supported applications. Toolkits such as GTK and Qt can expose accessibility information through bridges, subject to their configuration and implementation. An accessible object might represent a button, text field, document or container. Its bus name and object path identify a remote object whose lifetime is controlled by the application or bridge. An application closing a window can make an accessible object defunct while a client still has a cached proxy. A reference to a proxy does not keep the remote UI alive.
The AT-SPI 2.52.0 accessible implementation includes state handling for defunct objects. The accessibility bus and object tree are separate from a FileChooser request; not all accessibility access is mediated by desktop portals. Accessibility can expose text and actions that are sensitive in a different trust model than window pixels. A desktop's bus policy, application sandbox and bridge configuration affect what is available. A portal permission for one file does not automatically authorize inspection of every accessibility object, and the presence of an accessibility service does not itself establish a complete security boundary.
When a remote accessible object becomes defunct, an assistive client should handle method errors or the defunct state, discard stale cached information and avoid repeated operations on the dead object. Object paths can be reused after a process restarts, so a bus name, connection lifetime and object path all matter. An application that exposes its own accessible tree must keep role and state updates coherent with its UI and avoid leaking stale references. We do not query the host's accessibility bus or read any user's text in the exercise.
Permission scope and asynchronous ownership
A request can outlive the window that initiated it, so the initiating window is not a sufficient owner for every pending object. A toolkit may give the portal a parent-window identifier so a dialog can be associated with a window, but that identifier is not itself proof of user consent or a transferable file capability. If the parent disappears, the client must still release its subscription, callback data and any returned handles. Backend cancellation and the client's own shutdown can occur in either order, so the implementation needs an explicit terminal-state rule rather than assuming a single fixed event sequence.
Consider a document selected for reading and then replaced on disk. A previously opened fd refers to the opened file object, while a later lookup of the same path can refer to a different object or fail. A document-portal export can add another layer of identity and authorization. Applications should retain the object they actually intend to use and check operations for errors instead of using a pathname as proof that a previously approved object is unchanged. Likewise, exporting a document to one sandboxed application does not imply that another application receives the same permission.
For screen capture, a session close can race with a pending start response. A correct client handles a response that arrives after its UI was dismissed, closes any returned resource it no longer needs, and does not accidentally start a stream merely because an old callback still has data. The frontend, backend, user and consuming application can all fail independently. A model that has only a single success Boolean cannot explain which component owns the request or how to dispose of a late fd.
Practice#
This self-contained model simulates two terminal events for one request. It uses no bus or portal and contains no real file selection:
python3 - <<'PY'
state = 'pending'
for event in ('cancel response', 'window closes'):
if state == 'pending':
state = 'finished-cancelled'
print(event, '->', state)
else:
print(event, '-> ignored; already terminal')
assert state == 'finished-cancelled'
PY
Expected output shows one transition and one ignored late event. Reverse the two events and describe the additional close-request step that a real client would need; the model deliberately does not implement D-Bus delivery or fd ownership. As a source-only alternative, use the lesson42 sequence above and list the subscription, request object, result and fd owners at each state. For a successful fictional fd result, identify who closes it on parse failure. Deliver the state and ownership table; no real file or permission is needed.
Knowledge check#
- Why subscribe before making a portal request?
- Does a method return containing a request handle mean user approval?
- What does the
request_unexportexcerpt release? - Why does an open file fd not automatically authorize access to its parent directory?
- Is the AT-SPI object tree the same service as the FileChooser portal?
Answers#
- A response can arrive quickly; an early subscription reduces the chance of missing it. The client must still validate the actual handle and handle call failure.
- No. It identifies an asynchronous request; a later response reports the interface-specific outcome.
- It closes the stored fd when present, unexports the interface and drops the export's request reference. Other references may still exist.
- The fd refers to an already opened file object with particular access rights. Directory traversal and opening other files require separate authority.
- No. AT-SPI is a separate accessibility bus and object model, with its own lifetimes and trust implications.
Checkpoint#
Submit a request state table covering response-first, close-first and disconnect, plus the successful-fd ownership case. Pass when every terminal route releases local data exactly once and cancellation is not labeled approval. If not, retrace the request unexport source and repeat the model. This is practice, not certification.
Sources#
See portal request source, portal file chooser source, portal ScreenCast interface, and AT-SPI accessible implementation. The portal security model and Documents interface explain fd and document-export authority. Runtime backend and permission behavior vary by installation.