Private
Public Access
1
0

xpc: Fixes file handle explosion - drop fd after its copied via xpc

This commit is contained in:
2025-08-29 18:48:16 -06:00
parent eb4426e473
commit 0128723765
3 changed files with 80 additions and 58 deletions

View File

@@ -110,10 +110,10 @@ impl XpcAgent {
Message::Dictionary(map) => { Message::Dictionary(map) => {
let method = super::util::dict_get_str(&map, "method").or_else(|| super::util::dict_get_str(&map, "type")).unwrap_or_else(|| "<unknown>".to_string()); let method = super::util::dict_get_str(&map, "method").or_else(|| super::util::dict_get_str(&map, "type")).unwrap_or_else(|| "<unknown>".to_string());
log::trace!(target: LOG_TARGET, "XPC request received: {}", method); log::trace!(target: LOG_TARGET, "XPC request received: {}", method);
let response = rt_conn.block_on(super::rpc::dispatch(&agent_conn, &conns_for_handler, client, &map)); let result = rt_conn.block_on(super::rpc::dispatch(&agent_conn, &conns_for_handler, client, &map));
let reply = xpc_sys::xpc_dictionary_create_reply(msg); let reply = xpc_sys::xpc_dictionary_create_reply(msg);
if !reply.is_null() { if !reply.is_null() {
let payload = message_to_xpc_object(response); let payload = message_to_xpc_object(result.message);
let apply_block = ConcreteBlock::new(move |key: *const c_char, value: xpc_sys::xpc_object_t| { let apply_block = ConcreteBlock::new(move |key: *const c_char, value: xpc_sys::xpc_object_t| {
xpc_sys::xpc_dictionary_set_value(reply, key, value); xpc_sys::xpc_dictionary_set_value(reply, key, value);
}) })
@@ -124,6 +124,10 @@ impl XpcAgent {
xpc_sys::xpc_connection_send_message(client, reply); xpc_sys::xpc_connection_send_message(client, reply);
xpc_sys::xpc_release(payload); xpc_sys::xpc_release(payload);
xpc_sys::xpc_release(reply); xpc_sys::xpc_release(reply);
// Drop any cleanup resource now that payload is constructed and sent.
drop(result.cleanup);
log::trace!(target: LOG_TARGET, "XPC reply sent for method: {}", method); log::trace!(target: LOG_TARGET, "XPC reply sent for method: {}", method);
} else { } else {
log::warn!(target: LOG_TARGET, "No reply port for method: {}", method); log::warn!(target: LOG_TARGET, "No reply port for method: {}", method);

View File

@@ -2,3 +2,23 @@ pub mod agent;
pub mod interface; pub mod interface;
pub mod rpc; pub mod rpc;
pub mod util; pub mod util;
use std::any::Any;
use xpc_connection::Message;
/// Result of dispatching an XPC request: the message to send plus an optional
/// resource to keep alive until after the XPC payload is constructed.
pub struct DispatchResult {
pub message: Message,
pub cleanup: Option<Box<dyn Any + Send>>,
}
impl DispatchResult {
pub fn new(message: Message) -> Self {
Self { message, cleanup: None }
}
pub fn with_cleanup<T: Any + Send + 'static>(message: Message, cleanup: T) -> Self {
Self { message, cleanup: Some(Box::new(cleanup)) }
}
}

View File

@@ -7,22 +7,23 @@ use xpc_connection::Message;
use xpc_connection_sys as xpc_sys; use xpc_connection_sys as xpc_sys;
use super::util::*; use super::util::*;
use super::DispatchResult;
pub async fn dispatch( pub async fn dispatch(
agent: &XpcAgent, agent: &XpcAgent,
subscribers: &std::sync::Mutex<Vec<XpcConn>>, subscribers: &std::sync::Mutex<Vec<XpcConn>>,
current_client: xpc_sys::xpc_connection_t, current_client: xpc_sys::xpc_connection_t,
root: &HashMap<CString, Message>, root: &HashMap<CString, Message>,
) -> Message { ) -> DispatchResult {
let request_id = dict_get_str(root, "request_id"); let request_id = dict_get_str(root, "request_id");
let method = match dict_get_str(root, "method").or_else(|| dict_get_str(root, "type")) { let method = match dict_get_str(root, "method").or_else(|| dict_get_str(root, "type")) {
Some(m) => m, Some(m) => m,
None => { None => {
return attach_request_id( return DispatchResult::new(attach_request_id(
make_error_reply("InvalidRequest", "Missing method/type"), make_error_reply("InvalidRequest", "Missing method/type"),
request_id, request_id,
) ))
} }
}; };
@@ -35,9 +36,9 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new(); let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "GetVersionResponse"); dict_put_str(&mut reply, "type", "GetVersionResponse");
dict_put_str(&mut reply, "version", &version); dict_put_str(&mut reply, "version", &version);
Message::Dictionary(reply) DispatchResult::new(Message::Dictionary(reply))
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}, },
// GetConversations // GetConversations
@@ -84,34 +85,34 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new(); let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "GetConversationsResponse"); dict_put_str(&mut reply, "type", "GetConversationsResponse");
reply.insert(cstr("conversations"), Message::Array(items)); reply.insert(cstr("conversations"), Message::Array(items));
Message::Dictionary(reply) DispatchResult::new(Message::Dictionary(reply))
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
// Sync ops // Sync ops
"SyncConversationList" => match agent.send_event(Event::SyncConversationList).await { "SyncConversationList" => match agent.send_event(Event::SyncConversationList).await {
Ok(()) => make_ok_reply(), Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}, },
"SyncAllConversations" => match agent.send_event(Event::SyncAllConversations).await { "SyncAllConversations" => match agent.send_event(Event::SyncAllConversations).await {
Ok(()) => make_ok_reply(), Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}, },
"SyncConversation" => { "SyncConversation" => {
let conversation_id = match get_dictionary_field(root, "arguments") let conversation_id = match get_dictionary_field(root, "arguments")
.and_then(|m| dict_get_str(m, "conversation_id")) .and_then(|m| dict_get_str(m, "conversation_id"))
{ {
Some(id) => id, Some(id) => id,
None => return make_error_reply("InvalidRequest", "Missing conversation_id"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")),
}; };
match agent match agent
.send_event(|r| Event::SyncConversation(conversation_id, r)) .send_event(|r| Event::SyncConversation(conversation_id, r))
.await .await
{ {
Ok(()) => make_ok_reply(), Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -121,14 +122,14 @@ pub async fn dispatch(
.and_then(|m| dict_get_str(m, "conversation_id")) .and_then(|m| dict_get_str(m, "conversation_id"))
{ {
Some(id) => id, Some(id) => id,
None => return make_error_reply("InvalidRequest", "Missing conversation_id"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")),
}; };
match agent match agent
.send_event(|r| Event::MarkConversationAsRead(conversation_id, r)) .send_event(|r| Event::MarkConversationAsRead(conversation_id, r))
.await .await
{ {
Ok(()) => make_ok_reply(), Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -136,11 +137,11 @@ pub async fn dispatch(
"GetMessages" => { "GetMessages" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return make_error_reply("InvalidRequest", "Missing arguments"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")),
}; };
let conversation_id = match dict_get_str(args, "conversation_id") { let conversation_id = match dict_get_str(args, "conversation_id") {
Some(id) => id, Some(id) => id,
None => return make_error_reply("InvalidRequest", "Missing conversation_id"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")),
}; };
let last_message_id = dict_get_str(args, "last_message_id"); let last_message_id = dict_get_str(args, "last_message_id");
match agent match agent
@@ -213,27 +214,27 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new(); let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "GetMessagesResponse"); dict_put_str(&mut reply, "type", "GetMessagesResponse");
reply.insert(cstr("messages"), Message::Array(items)); reply.insert(cstr("messages"), Message::Array(items));
Message::Dictionary(reply) DispatchResult::new(Message::Dictionary(reply))
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
// Delete all // Delete all
"DeleteAllConversations" => match agent.send_event(Event::DeleteAllConversations).await { "DeleteAllConversations" => match agent.send_event(Event::DeleteAllConversations).await {
Ok(()) => make_ok_reply(), Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}, },
// SendMessage // SendMessage
"SendMessage" => { "SendMessage" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return make_error_reply("InvalidRequest", "Missing arguments"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")),
}; };
let conversation_id = match dict_get_str(args, "conversation_id") { let conversation_id = match dict_get_str(args, "conversation_id") {
Some(v) => v, Some(v) => v,
None => return make_error_reply("InvalidRequest", "Missing conversation_id"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")),
}; };
let text = dict_get_str(args, "text").unwrap_or_default(); let text = dict_get_str(args, "text").unwrap_or_default();
let attachment_guids: Vec<String> = match args.get(&cstr("attachment_guids")) { let attachment_guids: Vec<String> = match args.get(&cstr("attachment_guids")) {
@@ -254,9 +255,9 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new(); let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "SendMessageResponse"); dict_put_str(&mut reply, "type", "SendMessageResponse");
dict_put_str(&mut reply, "uuid", &uuid.to_string()); dict_put_str(&mut reply, "uuid", &uuid.to_string());
Message::Dictionary(reply) DispatchResult::new(Message::Dictionary(reply))
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -264,11 +265,11 @@ pub async fn dispatch(
"GetAttachmentInfo" => { "GetAttachmentInfo" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return make_error_reply("InvalidRequest", "Missing arguments"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")),
}; };
let attachment_id = match dict_get_str(args, "attachment_id") { let attachment_id = match dict_get_str(args, "attachment_id") {
Some(v) => v, Some(v) => v,
None => return make_error_reply("InvalidRequest", "Missing attachment_id"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")),
}; };
match agent match agent
.send_event(|r| Event::GetAttachment(attachment_id, r)) .send_event(|r| Event::GetAttachment(attachment_id, r))
@@ -297,9 +298,9 @@ pub async fn dispatch(
"preview_downloaded", "preview_downloaded",
&attachment.is_downloaded(true).to_string(), &attachment.is_downloaded(true).to_string(),
); );
Message::Dictionary(reply) DispatchResult::new(Message::Dictionary(reply))
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -307,11 +308,11 @@ pub async fn dispatch(
"OpenAttachmentFd" => { "OpenAttachmentFd" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return make_error_reply("InvalidRequest", "Missing arguments"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")),
}; };
let attachment_id = match dict_get_str(args, "attachment_id") { let attachment_id = match dict_get_str(args, "attachment_id") {
Some(v) => v, Some(v) => v,
None => return make_error_reply("InvalidRequest", "Missing attachment_id"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")),
}; };
let preview = dict_get_str(args, "preview") let preview = dict_get_str(args, "preview")
.map(|s| s == "true") .map(|s| s == "true")
@@ -327,22 +328,19 @@ pub async fn dispatch(
let path = attachment.get_path_for_preview(preview); let path = attachment.get_path_for_preview(preview);
match std::fs::OpenOptions::new().read(true).open(&path) { match std::fs::OpenOptions::new().read(true).open(&path) {
Ok(file) => { Ok(file) => {
use std::os::fd::AsRawFd;
let fd = file.as_raw_fd(); let fd = file.as_raw_fd();
// Keep file alive until after conversion to XPC
std::mem::forget(file);
// Return file descriptor in reply
let mut reply: XpcMap = HashMap::new(); let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse"); dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse");
reply.insert(cstr("fd"), Message::Fd(fd)); reply.insert(cstr("fd"), Message::Fd(fd));
Message::Dictionary(reply) DispatchResult { message: Message::Dictionary(reply), cleanup: Some(Box::new(file)) }
} }
Err(e) => make_error_reply("OpenFailed", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("OpenFailed", &format!("{}", e))),
} }
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -350,11 +348,11 @@ pub async fn dispatch(
"DownloadAttachment" => { "DownloadAttachment" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return make_error_reply("InvalidRequest", "Missing arguments"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")),
}; };
let attachment_id = match dict_get_str(args, "attachment_id") { let attachment_id = match dict_get_str(args, "attachment_id") {
Some(v) => v, Some(v) => v,
None => return make_error_reply("InvalidRequest", "Missing attachment_id"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")),
}; };
let preview = dict_get_str(args, "preview") let preview = dict_get_str(args, "preview")
.map(|s| s == "true") .map(|s| s == "true")
@@ -363,8 +361,8 @@ pub async fn dispatch(
.send_event(|r| Event::DownloadAttachment(attachment_id, preview, r)) .send_event(|r| Event::DownloadAttachment(attachment_id, preview, r))
.await .await
{ {
Ok(()) => make_ok_reply(), Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -373,11 +371,11 @@ pub async fn dispatch(
use std::path::PathBuf; use std::path::PathBuf;
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return make_error_reply("InvalidRequest", "Missing arguments"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")),
}; };
let path = match dict_get_str(args, "path") { let path = match dict_get_str(args, "path") {
Some(v) => v, Some(v) => v,
None => return make_error_reply("InvalidRequest", "Missing path"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path")),
}; };
match agent match agent
.send_event(|r| Event::UploadAttachment(PathBuf::from(path), r)) .send_event(|r| Event::UploadAttachment(PathBuf::from(path), r))
@@ -387,9 +385,9 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new(); let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "UploadAttachmentResponse"); dict_put_str(&mut reply, "type", "UploadAttachmentResponse");
dict_put_str(&mut reply, "upload_guid", &upload_guid); dict_put_str(&mut reply, "upload_guid", &upload_guid);
Message::Dictionary(reply) DispatchResult::new(Message::Dictionary(reply))
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -408,14 +406,14 @@ pub async fn dispatch(
"username", "username",
&settings.username.unwrap_or_default(), &settings.username.unwrap_or_default(),
); );
Message::Dictionary(reply) DispatchResult::new(Message::Dictionary(reply))
} }
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}, },
"UpdateSettings" => { "UpdateSettings" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return make_error_reply("InvalidRequest", "Missing arguments"), None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")),
}; };
let server_url = dict_get_str(args, "server_url"); let server_url = dict_get_str(args, "server_url");
let username = dict_get_str(args, "username"); let username = dict_get_str(args, "username");
@@ -428,8 +426,8 @@ pub async fn dispatch(
.send_event(|r| Event::UpdateSettings(settings, r)) .send_event(|r| Event::UpdateSettings(settings, r))
.await .await
{ {
Ok(()) => make_ok_reply(), Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
} }
} }
@@ -440,13 +438,13 @@ pub async fn dispatch(
list.push(XpcConn(current_client)); list.push(XpcConn(current_client));
log::trace!(target: LOG_TARGET, "Client subscribed to signals (total subscribers: {})", list.len()); log::trace!(target: LOG_TARGET, "Client subscribed to signals (total subscribers: {})", list.len());
} }
make_ok_reply() DispatchResult::new(make_ok_reply())
} }
// Unknown method fallback // Unknown method fallback
other => make_error_reply("UnknownMethod", other), other => DispatchResult::new(make_error_reply("UnknownMethod", other)),
}; };
response = attach_request_id(response, request_id); response.message = attach_request_id(response.message, request_id);
response response
} }