rustdesk/rustdesk · error · std::io::Error
drm: truncated SCM_RIGHTS control message (MSG_CTRUNC)
Error message
drm: truncated SCM_RIGHTS control message (MSG_CTRUNC)
What it means
drm_recvmsg reads from the _drm Unix socket expecting at most one fd via SCM_RIGHTS. If the kernel sets MSG_CTRUNC on the msghdr, the ancillary data did not fit in the control buffer and file descriptors were silently dropped; the function drops any received fd and returns this error because proceeding would leak or misattribute fds.
Source
Thrown at src/ipc/drm.rs:1239
let mut rawfd: libc::c_int = -1;
std::ptr::copy_nonoverlapping(
data.add(i * std::mem::size_of::<libc::c_int>()),
&mut rawfd as *mut libc::c_int as *mut u8,
std::mem::size_of::<libc::c_int>(),
);
if rawfd >= 0 {
let owned = OwnedFd::from_raw_fd(rawfd);
if got.is_none() {
got = Some(owned);
} // else: surplus fd, dropped here -> closed
}
}
}
cmsg = libc::CMSG_NXTHDR(&msg, cmsg);
}
if msg.msg_flags & libc::MSG_CTRUNC != 0 {
drop(got);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"drm: truncated SCM_RIGHTS control message (MSG_CTRUNC)",
));
}
Ok((n as usize, got))
}
async fn drm_write_all(
stream: &tokio::net::UnixStream,
mut buf: &[u8],
mut pass_fd: Option<RawFd>,
) -> ResultType<()> {
// ONE deadline for the whole write: arming it per readiness wait lets a dripping peer re-arm it.
let deadline =
tokio::time::Instant::now() + std::time::Duration::from_millis(DRM_SEND_TIMEOUT_MS);
while !buf.is_empty() {
match tokio::time::timeout_at(deadline, stream.writable()).await {
Ok(r) => r?,View on GitHub (pinned to 91c9fccbb0)
Solutions
- Ensure both ends use the same protocol: the sender must pass exactly one fd per frame (drm_sendmsg only supports one)
- Enlarge the receive control buffer so it can hold the maximum expected ancillary data
- Treat MSG_CTRUNC as fatal for the connection and reconnect, closing the received fds first (this code already drops them)
- Verify no other component on the same socket (proxy, forwarder) is batching multiple SCM_RIGHTS messages together
Defensive patterns
Strategy: fallback
Try / catch
match read_result {
Err(e) if e.to_string().contains("MSG_CTRUNC") => {
log::warn!("truncated ancillary data; closing and reconnecting");
reconnect_drm_channel();
}
other => other.map_err(|e| e.into()),
} Prevention
- Keep the one-fd-per-frame protocol identical on both ends; never batch multiple fds
- Recompute the control buffer size if the protocol ever carries more ancillary data
- Drop received fds before closing on any truncation to avoid fd leaks
- Integration-test fd passing between mismatched versions to catch buffer regressions
When it happens
Trigger: Calling drm_read_full -> drm_recvmsg when the peer attaches ancillary data larger than DRM_CMSG_CAP (e.g. multiple fds via SCM_RIGHTS) so the kernel truncates the control message (msg_flags & MSG_CTRUNC != 0).
Common situations: Protocol version mismatch where a peer sends several fds per frame but this side only allocates CMSG_SPACE for one; a buffer-size regression shrinking DRM_CMSG_CAP; a malicious/mismatched peer flooding the socket with fd-passing messages.
Related errors
- drm: CMSG_FIRSTHDR null
- drm: peer did not accept the remaining {} byte(s) within {DR
- drm: socket write returned 0 (peer closed)
- drm: socket closed by peer
- drm: _drm frame-ack write returned 0 (peer closed)
AI-assisted analysis of rustdesk/rustdesk@91c9fccbb0 (2026-09-10).
Data as JSON: /api/errors/b7174fafacd06340.
Report an issue: GitHub.