Before you start#
Read Wayland surfaces and events, input and seat routing, and desktop portals. The examples use Wayland 1.23.0 and wayland-protocols 1.36, with wlroots 0.18.0 as a separate implementation reference. The practice sends a self-authored string through an ordinary private pipe; it does not read or set a clipboard, capture a screen, or lock a session.
Learning goals#
Trace an offered MIME type through Wayland selection transfer and explain descriptor ownership and cancellation. Distinguish screen-capture authorization from session locking, and reason about output and locker failure without assuming all desktop clients possess privileged access.
Source walkthrough#
- Selection transfer
- A source writes requested MIME data to a receiver fd
- Screen capture
- A portal or privileged protocol mediates capture
- Session lock
- A trusted locker and compositor coordinate locked outputs
- Failure
- Close fds, cancel stale transfers and preserve lock policy
A clipboard is a user-facing idea, but the underlying protocols describe selection owners, offers and transfers rather than a universal shared string. In Wayland's core data-device protocol, a client creates a wl_data_source, advertises supported MIME types and can set a selection with a valid input serial. The compositor controls delivery of the resulting offer according to seat focus and policy. An offer tells a receiver which representations the source claims it can provide; it does not contain all the bytes or establish that the source will remain alive until a later read. See Wayland 1.23.0 protocol/wayland.xml.
The selection serial relates the request to an appropriate server-issued input event. It is not a globally reusable password or a guarantee that every selection request will be accepted. A compositor can change focus or selection while messages are in flight. A receiver must associate events with the correct offer object and destroy stale offers according to the protocol. A source receives cancellation when it is no longer the selection under the relevant rules; it must stop advertising that ownership and release its own state. A clipboard manager, if present and authorized, can implement persistence by obtaining data, but clipboard persistence after the source exits is not guaranteed by the basic offer alone.
The cancellation event is in the same Wayland 1.23.0 protocol/wayland.xml, MIT-style notice in the file (the drag-and-drop reasons that follow are omitted):
<event name="cancelled">
<description summary="selection was cancelled">
This data source is no longer valid. There are several reasons why
this could happen:
- The data source has been replaced by another data source.
The first listed reason is replacement by another source, so a source that receives this event should stop treating itself as the selection owner. The excerpt does not say whether a transfer already in flight can still complete, so a receiver still needs its own check on the descriptor it holds.
A MIME type identifies a representation such as UTF-8 text or an image format. It is not proof that the bytes are valid, safe to parse, or small. A receiver chooses an advertised type, creates a pipe or another suitable fd, and requests transfer. The compositor arranges for the source to receive a write fd; the source writes the chosen representation and closes its end. The receiver reads until EOF or error and closes its read end. Large data can arrive in partial reads or writes, and asynchronous I/O needs a lifetime for the fd, source/offer objects and callback data. Neither process should block the compositor's event loop indefinitely while waiting for a peer.
From Wayland 1.23.0 protocol/wayland.xml, wl_data_offer.receive, MIT-style copyright and license notice in the XML; the rest of the description is omitted:
The receiving client reads from the read end of the pipe until
EOF and then closes its end, at which point the transfer is
complete.
The first line identifies the receiving side and the stream endpoint. The second makes EOF the end of this byte stream and requires closing the receiver's endpoint. The final line describes transfer completion, not proof that the content was valid or successfully processed by the application. The surrounding protocol text states that the source writes the requested MIME representation and closes its fd. An fd transferred over a Unix socket gives the receiver a separate descriptor reference; closing a local copy does not automatically close every other process's copy.
- Source clientCompositor
wl_data_device.set_selection with serial
- CompositorReceiver client
wl_data_device.selection offer with MIME types
- Receiver clientCompositor
wl_data_offer.receive(mime, pipe write end)
- CompositorSource client
wl_data_source.send(mime, fd)
- Source clientPipe
writes bytes, then closes its write end
- Receiver clientPipe
reads until EOF, then closes read end
Time runs downward. Dashed arrows show returns.
Read the text version
- Source client → Compositor: wl_data_device.set_selection with serial
- Compositor → Receiver client: wl_data_device.selection offer with MIME types
- Receiver client → Compositor: wl_data_offer.receive(mime, pipe write end)
- Compositor → Source client: wl_data_source.send(mime, fd)
- Source client → Pipe: writes bytes, then closes its write end
- Receiver client → Pipe: reads until EOF, then closes read end
Failure and ownership of a selection
Imagine a source advertises text/plain;charset=utf-8 and a receiver requests it using a pipe. The receiver closes its local copy of the write end after handing off the needed reference, then reads from the read end. If the source writes hello and closes its writer, the receiver can read five bytes and then observe EOF. If the receiver exits and all read ends are closed before the source writes, the source can encounter EPIPE and, depending on signal handling, SIGPIPE. It must release its write fd and associated transfer state rather than assume the bytes were delivered. If the source dies before writing, the receiver may see an empty stream or an error depending on the exact descriptor state; it must not equate empty bytes with a successful semantic paste without its own checks.
An offer can become stale while a request or callback is queued. For example, selection A is replaced by selection B before the application handles an event about A. The application tracks which proxy and generation it is handling, destroys or ignores the old offer according to the protocol, and does not use a freed pointer merely because a callback was previously scheduled. A receiver must also bound allocations and validate data from another process. Clipboard data is untrusted input even when the two applications run under the same user; MIME labels do not sanitize HTML, image decoders or terminal escape sequences.
The primary-selection protocol, when available, provides a separate selection convention from the ordinary clipboard selection. It is an optional protocol and compositor feature, not a synonym for the core wl_data_device selection. Drag-and-drop also uses related data-source and offer objects but has different event sequencing and action negotiation. A client should not assume that setting one selection changes every other selection or that an offer can be used forever after focus and object changes. In wlroots 0.18.0, data-offer implementation provides one implementation reference; other compositors can implement different internal structures while following the applicable protocol.
X11 selections use a different client/server model, with selection ownership, conversion requests and property transfers. Xwayland integration can bridge selections between X11 and Wayland when supported by the compositor and environment. Traditional connected X11 clients can have broader opportunities to observe or interact with X resources than ordinary Wayland clients; this is not a claim that every X11 configuration has identical security policy. A bridge can fail or lose an owner, and a selection owner exiting can make a previously available conversion impossible. No host X selection, cookie or window is accessed here.
Screen capture is separate from clipboard transfer
A portal ScreenCast request involves a session, source selection, user or policy decision, and an fd-backed PipeWire connection where supported. A Screenshot interface can provide a different still-image workflow. A permission response is not itself an image, and a successful session setup does not prove a frame was delivered. Source availability, backend, policy and user choice determine results. A client must close its request and session objects, stream resources and fds when capture ends or is denied. See xdg-desktop-portal 1.18.4 ScreenCast XML.
Some compositors expose separate screencopy or capture protocols to selected clients. Such extensions may be privileged or filtered by compositor policy; their existence does not mean every client can capture every output. Kernel DRM device access is another authority boundary, and a process with broad device permissions may have capabilities independent of a portal. Conversely, a portal is not an operating-system-wide sandbox for unsandboxed applications. The appropriate security question is which client can access which source through which interface under the installed configuration, not whether one interface has a reassuring name.
A captured image or stream can contain private information. This lesson discusses ownership and policy without asking the reader to obtain screenshots, record a monitor, inspect windows or query a running portal. A private model of a pipe demonstrates byte transfer only; it cannot establish that a compositor filtered a capture request or that a user approved one. Source and protocol evidence can establish required message sequences, while actual permission policy requires an explicitly authorized environment and configuration-specific testing.
Session locking is a compositor contract
The ext-session-lock-v1 protocol allows a suitable locker client and compositor to coordinate a lock. The client requests a lock and creates lock surfaces for outputs as required by the protocol. The compositor retains authority over the session and decides whether to grant the request. While locked, normal session content must not become visible through an unprotected output according to the protocol's contract. A locker is trusted with deciding when to unlock; a random ordinary client should not automatically receive this authority. See wayland-protocols 1.36 ext-session-lock-v1.xml.
- Lock request
ext_session_lock_manager_v1.lock asks the compositor; no lock is held until a reply.
- Frames presented
Before locked, every output must present a new locked frame with no unlocked content possibly visible.
- locked event
Sent only after the frame condition; the locker treats the lock as active from here.
- unlock_and_destroy
Used after locked; the destroy request is the path when locked was never received.
- finished event
Compositor policy denies or ends the lock; it is never sent more than once.
From ext_session_lock_v1.locked in that XML, wayland-protocols 1.36, MIT-style file copyright and license notice; surrounding lifecycle text is omitted:
The locked event must not be sent until a new "locked" frame has been
presented on all outputs and no security sensitive normal/unlocked
content is possibly visible.
The first line requires a new locked frame on every output before this event. The second adds a confidentiality condition about normal content. Receiving the event is thus stronger than merely sending the lock request or constructing one surface. After the locked event, the protocol requires unlock_and_destroy rather than the ordinary destroy request; an invalid sequence is a protocol error. The compositor's implementation and hardware behavior still need verification in a real deployment. A protocol guarantee is not a claim that a particular machine was tested.
The request itself is in the same XML, under the lock object. In ext-session-lock-v1.xml, wayland-protocols 1.36, MIT-style notice in the file (surrounding protocol text omitted):
<request name="unlock_and_destroy" type="destructor">
<description summary="unlock the session, destroying the object">
This request indicates that the session should be unlocked, for
example because the user has entered their password and it has been
verified by the client.
This request also informs the compositor that the lock object will
no longer be used and should be destroyed. Existing objects created
through this interface remain valid.
After this request is made, lock surfaces created through this object
should be destroyed by the client as they will no longer be used by
the compositor.
It is a protocol error to make this request if the locked event has
not been sent. In that case, the lock object must be destroyed using
the destroy request.
Note that a correct client that wishes to exit directly after unlocking
the session must use the wl_display.sync request to ensure the server
receives and processes the unlock_and_destroy request. Otherwise
there is no guarantee that the server has unlocked the session due
to the asynchronous nature of the Wayland protocol. For example,
the server might terminate the client with a protocol error before
it processes the unlock_and_destroy request.
</description>
</request>
The description makes this request a protocol error before locked has been sent, and says a correct client should use wl_display.sync before it exits. The excerpt does not show the compositor's unlock policy or its handling of a disconnected locker, so it does not establish when the session is actually restored.
A lock surface has a role and a lifecycle associated with an output. Output addition, removal, surface configuration, rendering and client failure can occur while the lock is active. A compositor must maintain the protocol's security properties across these transitions, for example through its own blackout or secure fallback behavior where implemented. It must not reveal normal content simply because the locker has not yet drawn a new surface. The exact failure behavior is compositor-specific and must be checked in source and testing; this chapter does not claim every implementation chooses the same fallback.
The wlroots 0.18.0 session-lock implementation represents a protocol manager, lock and lock surfaces, with resource destruction and configure state. It is a library component used by a compositor, not an entire lock policy. Its source explicitly notes that lock-surface objects become inert when the corresponding lock is destroyed. Inert objects and destroyed resources still need their documented cleanup; an application must not continue treating them as active output surfaces. A compositor built with another toolkit has different internal ownership and scheduling.
A worked output-change timeline
Start with a synthetic session on outputs A and B. The locker requests a lock, creates the required surfaces, receives configuration and commits suitable content. The compositor presents protected frames on A and B; only after satisfying the protocol condition can it send locked. The client may now display its lock UI and decide when to request unlock under its trust and authentication policy. A request sent earlier was not proof of lock completion.
Next output C appears while the session is locked. The compositor must keep its security obligations while the locker and output state are updated. Suppose allocating a lock buffer for C fails. A secure implementation must not expose ordinary unlocked content on C; it needs whatever blackout or other safe behavior its implementation supplies. This is a conditional requirement and example, not a claim that a particular backend's allocation failure was tested. If the locker disconnects, the compositor must handle the abandoned lock according to protocol and policy; destroying a client process must not be casually equated with an authorized unlock.
When the user is legitimately authenticated through the locker's own mechanism, it sends unlock_and_destroy as required after a locked event. The compositor can then restore normal session presentation under its policy. Authentication itself can use separate services and is not specified by this protocol. The lock state, output frames, client connection, authentication result and normal session processes are separate lifetimes. A screen blanking timer that turns an output dark is not necessarily a lock and does not automatically block input or prevent later display of the session.
Input methods and privacy
Text-input protocols and input-method services can exchange composition state and surrounding text to support complex input. A compositor or input-method implementation controls which privileged interfaces are exposed. Virtual keyboards can inject or generate input through interfaces with their own authorization policy. Surrounding text may contain private material, so applications and input-method components should send only what their protocols and user expectations require, and should respect sensitive-field hints where applicable. These are separate interfaces from the clipboard selection and session-lock protocol.
An input method being active does not give every application permission to read another application's text. Conversely, a trusted input method or accessibility component can have a larger view of interaction than an ordinary client. The actual trust boundary depends on compositor policy, application sandbox, protocol versions and local configuration. This overview does not query input methods, inspect fields or record keystrokes.
Practice#
This Python program transfers only its own literal UTF-8 bytes through a private pipe. It is a pipe experiment, not a Wayland selection or clipboard test:
python3 - <<'PY'
import os
read_fd, write_fd = os.pipe()
os.write(write_fd, 'lesson'.encode('utf-8'))
os.close(write_fd)
data = b''
while True:
part = os.read(read_fd, 32)
if not part:
break
data += part
os.close(read_fd)
print('bytes:', len(data), 'text:', data.decode('utf-8'))
PY
Expected output is bytes: 6 text: lesson. For a failure counterexample, the following separate pipe has no reader; Python reports BrokenPipeError rather than changing a clipboard:
python3 - <<'PY'
import os
r, w = os.pipe()
os.close(r)
try:
os.write(w, b'lesson')
except BrokenPipeError:
print('writer saw EPIPE')
finally:
os.close(w)
PY
The source-only alternative is the five-byte hello transfer above: list the writer and reader references, the byte count, EOF and both close operations. Add the A/B/C lock timeline and identify why C's allocation failure cannot authorize revealing the normal session. Deliver both tables; neither requires a display or a real selection.
Knowledge check#
- What does a selection offer contain before a receiver asks for a MIME type?
- Why can a source receive
EPIPEduring a transfer? - What condition precedes the session-lock
lockedevent? - Does a capture portal's permission response itself contain a live screen stream?
- Why is a dark monitor not necessarily a locked session?
Answers#
- It advertises available representations and identifies an offer; the actual bytes are transferred separately when requested.
- If the receiver closes all read endpoints before the source writes, the pipe has no reader. The source must handle the error and release its fd.
- A new locked frame must have been presented on every output, with no security-sensitive normal content possibly visible, as the cited protocol states.
- No. Session setup, authorization and the subsequent stream connection are distinct operations with their own lifetimes and errors.
- Blanking can turn off or darken an output without authenticating the user or preventing normal session content and input from returning.
Checkpoint#
Submit the pipe ownership table and the three-output lock timeline. Pass when the pipe reaches EOF only after writer references close, EPIPE is handled, and a new output cannot expose normal content while locked. If not, reread the two XML excerpts and repeat the models. This is practice, not certification.
Sources#
Read the Wayland core data-device protocol, session-lock protocol, wlroots data offer, wlroots session lock, and portal ScreenCast interface. These are separate versioned references; no clipboard, screen or lock runtime was tested.