fix(native): potential sharablecontent icon/name crash (#10464)

This commit is contained in:
pengx17
2025-02-27 03:19:52 +00:00
parent e9484e8e15
commit be94f3fc17
6 changed files with 266 additions and 159 deletions

1
Cargo.lock generated
View File

@@ -69,6 +69,7 @@ dependencies = [
"core-foundation", "core-foundation",
"coreaudio-rs", "coreaudio-rs",
"dispatch2", "dispatch2",
"libc",
"napi", "napi",
"napi-build", "napi-build",
"napi-derive", "napi-derive",

View File

@@ -24,6 +24,7 @@ dispatch2 = "0.2"
dotenvy = "0.15" dotenvy = "0.15"
file-format = { version = "0.26", features = ["reader"] } file-format = { version = "0.26", features = ["reader"] }
homedir = "0.3" homedir = "0.3"
libc = "0.2"
mimalloc = "0.1" mimalloc = "0.1"
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] } napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" } napi-build = { version = "2" }

View File

@@ -1,7 +1,5 @@
/* eslint-disable @typescript-eslint/no-misused-promises */ /* eslint-disable @typescript-eslint/no-misused-promises */
import { exec } from 'node:child_process';
import { createServer } from 'node:http'; import { createServer } from 'node:http';
import { promisify } from 'node:util';
import { import {
type Application, type Application,
@@ -58,7 +56,7 @@ interface RecordingMetadata {
interface AppInfo { interface AppInfo {
app: Application; app: Application;
processId: number; processId: number;
processGroupId: number | null; processGroupId: number;
bundleIdentifier: string; bundleIdentifier: string;
name: string; name: string;
running: boolean; running: boolean;
@@ -226,8 +224,7 @@ async function startRecording(app: Application) {
return; return;
} }
// Find the root app of the process group const processGroupId = app.processGroupId;
const processGroupId = await getProcessGroupId(app.processId);
const rootApp = processGroupId const rootApp = processGroupId
? (shareableContent ? (shareableContent
.applications() .applications()
@@ -431,32 +428,16 @@ async function setupRecordingsWatcher() {
} }
} }
// Process management
async function getProcessGroupId(pid: number): Promise<number | null> {
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 // Application management
const shareableContent = new ShareableContent(); const shareableContent = new ShareableContent();
async function getAllApps(): Promise<AppInfo[]> { async function getAllApps(): Promise<AppInfo[]> {
const apps = await Promise.all( const apps = shareableContent.applications().map(app => {
shareableContent.applications().map(async app => {
try { try {
return { return {
app, app,
processId: app.processId, processId: app.processId,
processGroupId: await getProcessGroupId(app.processId), processGroupId: app.processGroupId,
bundleIdentifier: app.bundleIdentifier, bundleIdentifier: app.bundleIdentifier,
name: app.name, name: app.name,
running: app.isRunning, running: app.isRunning,
@@ -465,8 +446,7 @@ async function getAllApps(): Promise<AppInfo[]> {
console.error(error); console.error(error);
return null; return null;
} }
}) });
);
const filteredApps = apps.filter( const filteredApps = apps.filter(
(v): v is AppInfo => (v): v is AppInfo =>

View File

@@ -3,6 +3,7 @@
export declare class Application { export declare class Application {
static tapGlobalAudio(excludedProcesses: Array<Application> | undefined | null, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioTapStream static tapGlobalAudio(excludedProcesses: Array<Application> | undefined | null, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioTapStream
get processId(): number get processId(): number
get processGroupId(): number
get bundleIdentifier(): string get bundleIdentifier(): string
get name(): string get name(): string
get icon(): Buffer get icon(): Buffer

View File

@@ -16,6 +16,7 @@ block2 = { workspace = true }
core-foundation = { workspace = true, features = ["with-uuid"] } core-foundation = { workspace = true, features = ["with-uuid"] }
coreaudio-rs = { workspace = true } coreaudio-rs = { workspace = true }
dispatch2 = { workspace = true } dispatch2 = { workspace = true }
libc = { workspace = true }
objc2 = { workspace = true } objc2 = { workspace = true }
objc2-foundation = { workspace = true } objc2-foundation = { workspace = true }
screencapturekit = { workspace = true } screencapturekit = { workspace = true }

View File

@@ -20,6 +20,7 @@ use coreaudio::sys::{
AudioObjectAddPropertyListenerBlock, AudioObjectID, AudioObjectPropertyAddress, AudioObjectAddPropertyListenerBlock, AudioObjectID, AudioObjectPropertyAddress,
AudioObjectRemovePropertyListenerBlock, AudioObjectRemovePropertyListenerBlock,
}; };
use libc;
use napi::{ use napi::{
bindgen_prelude::{Buffer, Error, Float32Array, Result, Status}, bindgen_prelude::{Buffer, Error, Float32Array, Result, Status},
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
@@ -27,7 +28,6 @@ use napi::{
use napi_derive::napi; use napi_derive::napi;
use objc2::{ use objc2::{
msg_send, msg_send,
rc::Retained,
runtime::{AnyClass, AnyObject}, runtime::{AnyClass, AnyObject},
Encode, Encoding, Encode, Encoding,
}; };
@@ -114,51 +114,83 @@ impl TappableApplication {
} }
fn name(&self) -> Result<String> { fn name(&self) -> Result<String> {
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 // Get NSRunningApplication class with error handling
let running_app_class = NSRUNNING_APPLICATION_CLASS.as_ref().ok_or_else(|| { let running_app_class = match NSRUNNING_APPLICATION_CLASS.as_ref() {
Error::new( Some(class) => class,
Status::GenericFailure, None => {
"NSRunningApplication class not found", return Ok(String::new());
) }
})?; };
// Get running application with PID // Get running application with PID
let running_app: *mut AnyObject = let running_app: *mut AnyObject =
unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] }; unsafe { msg_send![*running_app_class, runningApplicationWithProcessIdentifier: pid] };
if running_app.is_null() { if running_app.is_null() {
return Ok(String::new()); 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 // Get localized name
let name: *mut NSString = unsafe { msg_send![running_app, localizedName] }; let name_ptr: *mut NSString = msg_send![running_app, localizedName];
if name.is_null() { if name_ptr.is_null() {
return Ok(String::new()); return Ok(String::new());
} }
// Create a safe wrapper and convert to string // Create a copy of the string without taking ownership of the NSString
let name = unsafe { let length: usize = msg_send![name_ptr, length];
Retained::from_raw(name).ok_or_else(|| { let utf8_ptr: *const u8 = msg_send![name_ptr, UTF8String];
Error::new(
Status::GenericFailure, if utf8_ptr.is_null() {
"Failed to create safe wrapper for localizedName", return Ok(String::new());
) }
})?
}; let bytes = std::slice::from_raw_parts(utf8_ptr, length);
Ok(name.to_string()) 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()),
}
} }
fn icon(&self) -> Result<Vec<u8>> { fn icon(&self) -> Result<Vec<u8>> {
let pid = self.process_id()?; // 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());
}
};
// Get NSRunningApplication class // Get NSRunningApplication class with error handling
let running_app_class = NSRUNNING_APPLICATION_CLASS.as_ref().ok_or_else(|| { let running_app_class = match NSRUNNING_APPLICATION_CLASS.as_ref() {
Error::new( Some(class) => class,
Status::GenericFailure, None => {
"NSRunningApplication class not found", return Ok(Vec::new());
) }
})?; };
// Get running application with PID // Get running application with PID
let running_app: *mut AnyObject = let running_app: *mut AnyObject =
@@ -175,11 +207,22 @@ impl TappableApplication {
} }
// Create a new NSImage with 64x64 size // Create a new NSImage with 64x64 size
let nsimage_class = AnyClass::get(c"NSImage") let nsimage_class = match AnyClass::get(c"NSImage") {
.ok_or_else(|| Error::new(Status::GenericFailure, "NSImage class not found"))?; Some(class) => class,
None => return Ok(Vec::new()),
};
let resized_image: *mut AnyObject = msg_send![nsimage_class, alloc]; let resized_image: *mut AnyObject = msg_send![nsimage_class, alloc];
if resized_image.is_null() {
return Ok(Vec::new());
}
let resized_image: *mut AnyObject = let resized_image: *mut AnyObject =
msg_send![resized_image, initWithSize: NSSize { width: 64.0, height: 64.0 }]; 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]; let _: () = msg_send![resized_image, lockFocus];
// Define drawing rectangle for 64x64 image // Define drawing rectangle for 64x64 image
@@ -202,27 +245,39 @@ impl TappableApplication {
} }
// Create bitmap image rep from TIFF // Create bitmap image rep from TIFF
let bitmap_class = AnyClass::get(c"NSBitmapImageRep") let bitmap_class = match AnyClass::get(c"NSBitmapImageRep") {
.ok_or_else(|| Error::new(Status::GenericFailure, "NSBitmapImageRep class not found"))?; Some(class) => class,
None => return Ok(Vec::new()),
};
let bitmap: *mut AnyObject = msg_send![bitmap_class, imageRepWithData: tiff_data]; let bitmap: *mut AnyObject = msg_send![bitmap_class, imageRepWithData: tiff_data];
if bitmap.is_null() { if bitmap.is_null() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
// Create properties dictionary with compression factor // Create properties dictionary with compression factor
let dict_class = AnyClass::get(c"NSMutableDictionary").ok_or_else(|| { let dict_class = match AnyClass::get(c"NSMutableDictionary") {
Error::new( Some(class) => class,
Status::GenericFailure, None => return Ok(Vec::new()),
"NSMutableDictionary class not found", };
)
})?;
let properties: *mut AnyObject = msg_send![dict_class, dictionary]; let properties: *mut AnyObject = msg_send![dict_class, dictionary];
if properties.is_null() {
return Ok(Vec::new());
}
// Add compression properties // Add compression properties
let compression_key = NSString::from_str("NSImageCompressionFactor"); let compression_key = NSString::from_str("NSImageCompressionFactor");
let number_class = AnyClass::get(c"NSNumber") let number_class = match AnyClass::get(c"NSNumber") {
.ok_or_else(|| Error::new(Status::GenericFailure, "NSNumber class not found"))?; Some(class) => class,
None => return Ok(Vec::new()),
};
let compression_value: *mut AnyObject = msg_send![number_class, numberWithDouble: 0.8]; 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]; let _: () = msg_send![properties, setObject: compression_value, forKey: &*compression_key];
// Get PNG data with properties // Get PNG data with properties
@@ -241,10 +296,46 @@ impl TappableApplication {
return Ok(Vec::new()); return Ok(Vec::new());
} }
// Copy bytes into a Vec<u8> // Copy bytes into a Vec<u8> instead of using the original memory
let data = std::slice::from_raw_parts(bytes, length).to_vec(); let data = std::slice::from_raw_parts(bytes, length).to_vec();
Ok(data) 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<i32> {
// 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);
}
Ok(pgid)
});
// Handle any panics
match pgid_result {
Ok(result) => result,
Err(_) => Ok(-1),
}
} }
} }
@@ -253,6 +344,7 @@ pub struct Application {
inner: TappableApplication, inner: TappableApplication,
pub(crate) object_id: AudioObjectID, pub(crate) object_id: AudioObjectID,
pub(crate) process_id: i32, pub(crate) process_id: i32,
pub(crate) process_group_id: i32,
pub(crate) bundle_identifier: String, pub(crate) bundle_identifier: String,
pub(crate) name: String, pub(crate) name: String,
} }
@@ -264,11 +356,13 @@ impl Application {
let bundle_identifier = app.bundle_identifier()?; let bundle_identifier = app.bundle_identifier()?;
let name = app.name()?; let name = app.name()?;
let process_id = app.process_id()?; let process_id = app.process_id()?;
let process_group_id = app.process_group_id()?;
Ok(Self { Ok(Self {
inner: app, inner: app,
object_id, object_id,
process_id, process_id,
process_group_id,
bundle_identifier, bundle_identifier,
name, name,
}) })
@@ -294,6 +388,11 @@ impl Application {
self.process_id self.process_id
} }
#[napi(getter)]
pub fn process_group_id(&self) -> i32 {
self.process_group_id
}
#[napi(getter)] #[napi(getter)]
pub fn bundle_identifier(&self) -> String { pub fn bundle_identifier(&self) -> String {
self.bundle_identifier.clone() self.bundle_identifier.clone()
@@ -306,16 +405,40 @@ impl Application {
#[napi(getter)] #[napi(getter)]
pub fn icon(&self) -> Result<Buffer> { pub fn icon(&self) -> Result<Buffer> {
let icon = self.inner.icon()?; // Use catch_unwind to prevent any panics
Ok(Buffer::from(icon)) let result = std::panic::catch_unwind(|| match self.inner.icon() {
Ok(icon) => Ok(Buffer::from(icon)),
Err(_) => Ok(Buffer::from(Vec::<u8>::new())),
});
// Handle any panics
match result {
Ok(result) => result,
Err(_) => Ok(Buffer::from(Vec::<u8>::new())),
}
} }
#[napi(getter)] #[napi(getter)]
pub fn get_is_running(&self) -> Result<bool> { pub fn get_is_running(&self) -> Result<bool> {
Ok(get_process_property( // Use catch_unwind to prevent any panics
&self.object_id, let result = std::panic::catch_unwind(|| {
kAudioProcessPropertyIsRunningInput, 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] #[napi]