From be94f3fc17e0c5bfe536742d6a7d569fdb9199d3 Mon Sep 17 00:00:00 2001 From: pengx17 Date: Thu, 27 Feb 2025 03:19:52 +0000 Subject: [PATCH] fix(native): potential sharablecontent icon/name crash (#10464) --- Cargo.lock | 1 + Cargo.toml | 1 + .../media-capture-playground/server/main.ts | 54 +-- packages/frontend/native/index.d.ts | 1 + .../frontend/native/media_capture/Cargo.toml | 1 + .../src/macos/screen_capture_kit.rs | 367 ++++++++++++------ 6 files changed, 266 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56bb45bce..a6d308460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,7 @@ dependencies = [ "core-foundation", "coreaudio-rs", "dispatch2", + "libc", "napi", "napi-build", "napi-derive", diff --git a/Cargo.toml b/Cargo.toml index 0c888d98a..65c9eeb22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ dispatch2 = "0.2" dotenvy = "0.15" file-format = { version = "0.26", features = ["reader"] } homedir = "0.3" +libc = "0.2" mimalloc = "0.1" napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] } napi-build = { version = "2" } diff --git a/packages/frontend/media-capture-playground/server/main.ts b/packages/frontend/media-capture-playground/server/main.ts index e132447d6..2d8a64778 100644 --- a/packages/frontend/media-capture-playground/server/main.ts +++ b/packages/frontend/media-capture-playground/server/main.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ -import { exec } from 'node:child_process'; import { createServer } from 'node:http'; -import { promisify } from 'node:util'; import { type Application, @@ -58,7 +56,7 @@ interface RecordingMetadata { interface AppInfo { app: Application; processId: number; - processGroupId: number | null; + processGroupId: number; bundleIdentifier: string; name: string; running: boolean; @@ -226,8 +224,7 @@ async function startRecording(app: Application) { return; } - // Find the root app of the process group - const processGroupId = await getProcessGroupId(app.processId); + const processGroupId = app.processGroupId; const rootApp = processGroupId ? (shareableContent .applications() @@ -431,42 +428,25 @@ async function setupRecordingsWatcher() { } } -// Process management -async function getProcessGroupId(pid: number): Promise { - try { - const execAsync = promisify(exec); - const { stdout } = await execAsync(`ps -o pgid -p ${pid}`); - const lines = stdout.trim().split('\n'); - if (lines.length < 2) return null; - - const pgid = parseInt(lines[1].trim(), 10); - return isNaN(pgid) ? null : pgid; - } catch { - return null; - } -} - // Application management const shareableContent = new ShareableContent(); async function getAllApps(): Promise { - const apps = await Promise.all( - shareableContent.applications().map(async app => { - try { - return { - app, - processId: app.processId, - processGroupId: await getProcessGroupId(app.processId), - bundleIdentifier: app.bundleIdentifier, - name: app.name, - running: app.isRunning, - }; - } catch (error) { - console.error(error); - return null; - } - }) - ); + const apps = shareableContent.applications().map(app => { + try { + return { + app, + processId: app.processId, + processGroupId: app.processGroupId, + bundleIdentifier: app.bundleIdentifier, + name: app.name, + running: app.isRunning, + }; + } catch (error) { + console.error(error); + return null; + } + }); const filteredApps = apps.filter( (v): v is AppInfo => diff --git a/packages/frontend/native/index.d.ts b/packages/frontend/native/index.d.ts index 09a573ad5..ec88d51d5 100644 --- a/packages/frontend/native/index.d.ts +++ b/packages/frontend/native/index.d.ts @@ -3,6 +3,7 @@ export declare class Application { static tapGlobalAudio(excludedProcesses: Array | undefined | null, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioTapStream get processId(): number + get processGroupId(): number get bundleIdentifier(): string get name(): string get icon(): Buffer diff --git a/packages/frontend/native/media_capture/Cargo.toml b/packages/frontend/native/media_capture/Cargo.toml index 222cd6588..9dbffcf15 100644 --- a/packages/frontend/native/media_capture/Cargo.toml +++ b/packages/frontend/native/media_capture/Cargo.toml @@ -16,6 +16,7 @@ block2 = { workspace = true } core-foundation = { workspace = true, features = ["with-uuid"] } coreaudio-rs = { workspace = true } dispatch2 = { workspace = true } +libc = { workspace = true } objc2 = { workspace = true } objc2-foundation = { workspace = true } screencapturekit = { workspace = true } diff --git a/packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs b/packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs index 45b720104..0c8f5a728 100644 --- a/packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs +++ b/packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs @@ -20,6 +20,7 @@ use coreaudio::sys::{ AudioObjectAddPropertyListenerBlock, AudioObjectID, AudioObjectPropertyAddress, AudioObjectRemovePropertyListenerBlock, }; +use libc; use napi::{ bindgen_prelude::{Buffer, Error, Float32Array, Result, Status}, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, @@ -27,7 +28,6 @@ use napi::{ use napi_derive::napi; use objc2::{ msg_send, - rc::Retained, runtime::{AnyClass, AnyObject}, Encode, Encoding, }; @@ -114,136 +114,227 @@ impl TappableApplication { } fn name(&self) -> Result { - let pid = self.process_id()?; + // Use catch_unwind to prevent any panics + let name_result = std::panic::catch_unwind(|| { + // Get process ID with error handling + let pid = match self.process_id() { + Ok(pid) => pid, + Err(_) => { + return Ok(String::new()); + } + }; - // Get NSRunningApplication class - let running_app_class = NSRUNNING_APPLICATION_CLASS.as_ref().ok_or_else(|| { - Error::new( - Status::GenericFailure, - "NSRunningApplication class not found", - ) - })?; + // Get NSRunningApplication class with error handling + let running_app_class = match NSRUNNING_APPLICATION_CLASS.as_ref() { + Some(class) => class, + None => { + return Ok(String::new()); + } + }; - // Get running application with PID - let running_app: *mut AnyObject = - unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] }; - if running_app.is_null() { - return Ok(String::new()); + // Get running application with PID + let running_app: *mut AnyObject = + unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] }; + + if running_app.is_null() { + return Ok(String::new()); + } + + // Instead of using Retained::from_raw which takes ownership, + // we'll just copy the string value and let the Objective-C runtime + // handle the memory management of the original object + unsafe { + // Get localized name + let name_ptr: *mut NSString = msg_send![running_app, localizedName]; + if name_ptr.is_null() { + return Ok(String::new()); + } + + // Create a copy of the string without taking ownership of the NSString + let length: usize = msg_send![name_ptr, length]; + let utf8_ptr: *const u8 = msg_send![name_ptr, UTF8String]; + + if utf8_ptr.is_null() { + return Ok(String::new()); + } + + let bytes = std::slice::from_raw_parts(utf8_ptr, length); + match std::str::from_utf8(bytes) { + Ok(s) => Ok(s.to_string()), + Err(_) => Ok(String::new()), + } + } + }); + + // Handle any panics that might have occurred + match name_result { + Ok(result) => result, + Err(_) => Ok(String::new()), } - - // Get localized name - let name: *mut NSString = unsafe { msg_send![running_app, localizedName] }; - if name.is_null() { - return Ok(String::new()); - } - - // Create a safe wrapper and convert to string - let name = unsafe { - Retained::from_raw(name).ok_or_else(|| { - Error::new( - Status::GenericFailure, - "Failed to create safe wrapper for localizedName", - ) - })? - }; - Ok(name.to_string()) } fn icon(&self) -> Result> { - let pid = self.process_id()?; - - // Get NSRunningApplication class - let running_app_class = NSRUNNING_APPLICATION_CLASS.as_ref().ok_or_else(|| { - Error::new( - Status::GenericFailure, - "NSRunningApplication class not found", - ) - })?; - - // Get running application with PID - let running_app: *mut AnyObject = - unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] }; - if running_app.is_null() { - return Ok(Vec::new()); - } - - unsafe { - // Get original icon - let icon: *mut AnyObject = msg_send![running_app, icon]; - if icon.is_null() { - return Ok(Vec::new()); - } - - // Create a new NSImage with 64x64 size - let nsimage_class = AnyClass::get(c"NSImage") - .ok_or_else(|| Error::new(Status::GenericFailure, "NSImage class not found"))?; - let resized_image: *mut AnyObject = msg_send![nsimage_class, alloc]; - let resized_image: *mut AnyObject = - msg_send![resized_image, initWithSize: NSSize { width: 64.0, height: 64.0 }]; - let _: () = msg_send![resized_image, lockFocus]; - - // Define drawing rectangle for 64x64 image - let draw_rect = NSRect { - origin: NSPoint { x: 0.0, y: 0.0 }, - size: NSSize { - width: 64.0, - height: 64.0, - }, + // Use catch_unwind to prevent any panics + let icon_result = std::panic::catch_unwind(|| { + // Get process ID with error handling + let pid = match self.process_id() { + Ok(pid) => pid, + Err(_) => { + return Ok(Vec::new()); + } }; - // Draw the original icon into draw_rect (using NSCompositingOperationCopy = 2) - let _: () = msg_send![icon, drawInRect: draw_rect, fromRect: NSRect { origin: NSPoint { x: 0.0, y: 0.0 }, size: NSSize { width: 0.0, height: 0.0 } }, operation: 2, fraction: 1.0]; - let _: () = msg_send![resized_image, unlockFocus]; + // Get NSRunningApplication class with error handling + let running_app_class = match NSRUNNING_APPLICATION_CLASS.as_ref() { + Some(class) => class, + None => { + return Ok(Vec::new()); + } + }; - // Get TIFF representation from the downsized image - let tiff_data: *mut AnyObject = msg_send![resized_image, TIFFRepresentation]; - if tiff_data.is_null() { + // Get running application with PID + let running_app: *mut AnyObject = + unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] }; + if running_app.is_null() { return Ok(Vec::new()); } - // Create bitmap image rep from TIFF - let bitmap_class = AnyClass::get(c"NSBitmapImageRep") - .ok_or_else(|| Error::new(Status::GenericFailure, "NSBitmapImageRep class not found"))?; - let bitmap: *mut AnyObject = msg_send![bitmap_class, imageRepWithData: tiff_data]; - if bitmap.is_null() { - return Ok(Vec::new()); + unsafe { + // Get original icon + let icon: *mut AnyObject = msg_send![running_app, icon]; + if icon.is_null() { + return Ok(Vec::new()); + } + + // Create a new NSImage with 64x64 size + let nsimage_class = match AnyClass::get(c"NSImage") { + Some(class) => class, + None => return Ok(Vec::new()), + }; + + let resized_image: *mut AnyObject = msg_send![nsimage_class, alloc]; + if resized_image.is_null() { + return Ok(Vec::new()); + } + + let resized_image: *mut AnyObject = + msg_send![resized_image, initWithSize: NSSize { width: 64.0, height: 64.0 }]; + if resized_image.is_null() { + return Ok(Vec::new()); + } + + let _: () = msg_send![resized_image, lockFocus]; + + // Define drawing rectangle for 64x64 image + let draw_rect = NSRect { + origin: NSPoint { x: 0.0, y: 0.0 }, + size: NSSize { + width: 64.0, + height: 64.0, + }, + }; + + // Draw the original icon into draw_rect (using NSCompositingOperationCopy = 2) + let _: () = msg_send![icon, drawInRect: draw_rect, fromRect: NSRect { origin: NSPoint { x: 0.0, y: 0.0 }, size: NSSize { width: 0.0, height: 0.0 } }, operation: 2, fraction: 1.0]; + let _: () = msg_send![resized_image, unlockFocus]; + + // Get TIFF representation from the downsized image + let tiff_data: *mut AnyObject = msg_send![resized_image, TIFFRepresentation]; + if tiff_data.is_null() { + return Ok(Vec::new()); + } + + // Create bitmap image rep from TIFF + let bitmap_class = match AnyClass::get(c"NSBitmapImageRep") { + Some(class) => class, + None => return Ok(Vec::new()), + }; + + let bitmap: *mut AnyObject = msg_send![bitmap_class, imageRepWithData: tiff_data]; + if bitmap.is_null() { + return Ok(Vec::new()); + } + + // Create properties dictionary with compression factor + let dict_class = match AnyClass::get(c"NSMutableDictionary") { + Some(class) => class, + None => return Ok(Vec::new()), + }; + + let properties: *mut AnyObject = msg_send![dict_class, dictionary]; + if properties.is_null() { + return Ok(Vec::new()); + } + + // Add compression properties + let compression_key = NSString::from_str("NSImageCompressionFactor"); + let number_class = match AnyClass::get(c"NSNumber") { + Some(class) => class, + None => return Ok(Vec::new()), + }; + + let compression_value: *mut AnyObject = msg_send![number_class, numberWithDouble: 0.8]; + if compression_value.is_null() { + return Ok(Vec::new()); + } + + let _: () = msg_send![properties, setObject: compression_value, forKey: &*compression_key]; + + // Get PNG data with properties + let png_data: *mut AnyObject = + msg_send![bitmap, representationUsingType: 4, properties: properties]; // 4 = PNG + + if png_data.is_null() { + return Ok(Vec::new()); + } + + // Get bytes from NSData + let bytes: *const u8 = msg_send![png_data, bytes]; + let length: usize = msg_send![png_data, length]; + + if bytes.is_null() { + return Ok(Vec::new()); + } + + // Copy bytes into a Vec instead of using the original memory + let data = std::slice::from_raw_parts(bytes, length).to_vec(); + Ok(data) + } + }); + + // Handle any panics that might have occurred + match icon_result { + Ok(result) => result, + Err(_) => Ok(Vec::new()), + } + } + + fn process_group_id(&self) -> Result { + // Use catch_unwind to prevent any panics + let pgid_result = std::panic::catch_unwind(|| { + // First get the process ID + let pid = match self.process_id() { + Ok(pid) => pid, + Err(_) => { + return Ok(-1); // Return -1 for error cases + } + }; + + // Call libc's getpgid function to get the process group ID + let pgid = unsafe { libc::getpgid(pid) }; + + // getpgid returns -1 on error + if pgid == -1 { + return Ok(-1); } - // Create properties dictionary with compression factor - let dict_class = AnyClass::get(c"NSMutableDictionary").ok_or_else(|| { - Error::new( - Status::GenericFailure, - "NSMutableDictionary class not found", - ) - })?; - let properties: *mut AnyObject = msg_send![dict_class, dictionary]; + Ok(pgid) + }); - // Add compression properties - let compression_key = NSString::from_str("NSImageCompressionFactor"); - let number_class = AnyClass::get(c"NSNumber") - .ok_or_else(|| Error::new(Status::GenericFailure, "NSNumber class not found"))?; - let compression_value: *mut AnyObject = msg_send![number_class, numberWithDouble: 0.8]; - let _: () = msg_send![properties, setObject: compression_value, forKey: &*compression_key]; - - // Get PNG data with properties - let png_data: *mut AnyObject = - msg_send![bitmap, representationUsingType: 4, properties: properties]; // 4 = PNG - - if png_data.is_null() { - return Ok(Vec::new()); - } - - // Get bytes from NSData - let bytes: *const u8 = msg_send![png_data, bytes]; - let length: usize = msg_send![png_data, length]; - - if bytes.is_null() { - return Ok(Vec::new()); - } - - // Copy bytes into a Vec - let data = std::slice::from_raw_parts(bytes, length).to_vec(); - Ok(data) + // Handle any panics + match pgid_result { + Ok(result) => result, + Err(_) => Ok(-1), } } } @@ -253,6 +344,7 @@ pub struct Application { inner: TappableApplication, pub(crate) object_id: AudioObjectID, pub(crate) process_id: i32, + pub(crate) process_group_id: i32, pub(crate) bundle_identifier: String, pub(crate) name: String, } @@ -264,11 +356,13 @@ impl Application { let bundle_identifier = app.bundle_identifier()?; let name = app.name()?; let process_id = app.process_id()?; + let process_group_id = app.process_group_id()?; Ok(Self { inner: app, object_id, process_id, + process_group_id, bundle_identifier, name, }) @@ -294,6 +388,11 @@ impl Application { self.process_id } + #[napi(getter)] + pub fn process_group_id(&self) -> i32 { + self.process_group_id + } + #[napi(getter)] pub fn bundle_identifier(&self) -> String { self.bundle_identifier.clone() @@ -306,16 +405,40 @@ impl Application { #[napi(getter)] pub fn icon(&self) -> Result { - let icon = self.inner.icon()?; - Ok(Buffer::from(icon)) + // Use catch_unwind to prevent any panics + let result = std::panic::catch_unwind(|| match self.inner.icon() { + Ok(icon) => Ok(Buffer::from(icon)), + Err(_) => Ok(Buffer::from(Vec::::new())), + }); + + // Handle any panics + match result { + Ok(result) => result, + Err(_) => Ok(Buffer::from(Vec::::new())), + } } #[napi(getter)] pub fn get_is_running(&self) -> Result { - Ok(get_process_property( - &self.object_id, - kAudioProcessPropertyIsRunningInput, - )?) + // Use catch_unwind to prevent any panics + let result = std::panic::catch_unwind(|| { + match get_process_property(&self.object_id, kAudioProcessPropertyIsRunningInput) { + Ok(is_running) => Ok(is_running), + Err(_) => { + // Default to true to avoid potential issues + Ok(true) + } + } + }); + + // Handle any panics + match result { + Ok(result) => result, + Err(_) => { + // Default to true to avoid potential issues + Ok(true) + } + } } #[napi]