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) => {
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);
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);
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| {
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_release(payload);
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);
} else {
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 rpc;
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 super::util::*;
use super::DispatchResult;
pub async fn dispatch(
agent: &XpcAgent,
subscribers: &std::sync::Mutex<Vec<XpcConn>>,
current_client: xpc_sys::xpc_connection_t,
root: &HashMap<CString, Message>,
) -> Message {
) -> DispatchResult {
let request_id = dict_get_str(root, "request_id");
let method = match dict_get_str(root, "method").or_else(|| dict_get_str(root, "type")) {
Some(m) => m,
None => {
return attach_request_id(
return DispatchResult::new(attach_request_id(
make_error_reply("InvalidRequest", "Missing method/type"),
request_id,
)
))
}
};
@@ -35,9 +36,9 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "GetVersionResponse");
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
@@ -84,34 +85,34 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "GetConversationsResponse");
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
"SyncConversationList" => match agent.send_event(Event::SyncConversationList).await {
Ok(()) => make_ok_reply(),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
},
"SyncAllConversations" => match agent.send_event(Event::SyncAllConversations).await {
Ok(()) => make_ok_reply(),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
},
"SyncConversation" => {
let conversation_id = match get_dictionary_field(root, "arguments")
.and_then(|m| dict_get_str(m, "conversation_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
.send_event(|r| Event::SyncConversation(conversation_id, r))
.await
{
Ok(()) => make_ok_reply(),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
Ok(()) => DispatchResult::new(make_ok_reply()),
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"))
{
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
.send_event(|r| Event::MarkConversationAsRead(conversation_id, r))
.await
{
Ok(()) => make_ok_reply(),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}
}
@@ -136,11 +137,11 @@ pub async fn dispatch(
"GetMessages" => {
let args = match get_dictionary_field(root, "arguments") {
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") {
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");
match agent
@@ -213,27 +214,27 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "GetMessagesResponse");
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
"DeleteAllConversations" => match agent.send_event(Event::DeleteAllConversations).await {
Ok(()) => make_ok_reply(),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
},
// SendMessage
"SendMessage" => {
let args = match get_dictionary_field(root, "arguments") {
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") {
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 attachment_guids: Vec<String> = match args.get(&cstr("attachment_guids")) {
@@ -254,9 +255,9 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "SendMessageResponse");
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" => {
let args = match get_dictionary_field(root, "arguments") {
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") {
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
.send_event(|r| Event::GetAttachment(attachment_id, r))
@@ -297,9 +298,9 @@ pub async fn dispatch(
"preview_downloaded",
&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" => {
let args = match get_dictionary_field(root, "arguments") {
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") {
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")
.map(|s| s == "true")
@@ -327,22 +328,19 @@ pub async fn dispatch(
let path = attachment.get_path_for_preview(preview);
match std::fs::OpenOptions::new().read(true).open(&path) {
Ok(file) => {
use std::os::fd::AsRawFd;
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();
dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse");
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" => {
let args = match get_dictionary_field(root, "arguments") {
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") {
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")
.map(|s| s == "true")
@@ -363,8 +361,8 @@ pub async fn dispatch(
.send_event(|r| Event::DownloadAttachment(attachment_id, preview, r))
.await
{
Ok(()) => make_ok_reply(),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}
}
@@ -373,11 +371,11 @@ pub async fn dispatch(
use std::path::PathBuf;
let args = match get_dictionary_field(root, "arguments") {
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") {
Some(v) => v,
None => return make_error_reply("InvalidRequest", "Missing path"),
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path")),
};
match agent
.send_event(|r| Event::UploadAttachment(PathBuf::from(path), r))
@@ -387,9 +385,9 @@ pub async fn dispatch(
let mut reply: XpcMap = HashMap::new();
dict_put_str(&mut reply, "type", "UploadAttachmentResponse");
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",
&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" => {
let args = match get_dictionary_field(root, "arguments") {
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 username = dict_get_str(args, "username");
@@ -428,8 +426,8 @@ pub async fn dispatch(
.send_event(|r| Event::UpdateSettings(settings, r))
.await
{
Ok(()) => make_ok_reply(),
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
Ok(()) => DispatchResult::new(make_ok_reply()),
Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
}
}
@@ -440,13 +438,13 @@ pub async fn dispatch(
list.push(XpcConn(current_client));
log::trace!(target: LOG_TARGET, "Client subscribed to signals (total subscribers: {})", list.len());
}
make_ok_reply()
DispatchResult::new(make_ok_reply())
}
// 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
}