fix(oakengine): facade bugs found by integration tests

undo:
- NULL/empty label no longer crosses to oakundo as a dangling 0x1
  pointer (push, group_begin/end) — fixed SIGSEGV
- group_abort now undoes each executed child in reverse order

task:
- create_project_import addrefs the borrowed project handle instead of
  freeing it under the async task — fixed UAF/SIGSEGV

timeline:
- toggle_enabled/delete_clips guard NULL+0 slices — fixed SIGABRT
- BlockSplitCommand halves placed correctly (oaktimeline undosplit)
- PreservingLinks / ripple remove / ripple delete-gaps commands
  self-prepare on first redo — fixes silent no-op split/ripple
- trim_clips_to targets the block containing the point, not the track
- delete_empty_tracks applies the live track removal
- ripple facades no longer free borrowed track handles still referenced
  by commands — fixed UAF

node:
- project_add_node releases the factory handle — fixes per-call leak
- inputs_from(recursive=0) matches direct feeders (BFS off-by-one)
- group passthrough id/resolve treat two-stage string length as success
- node_connect(_command) reject duplicate connects with E_STATE
- folder_add_child enforces one-folder-per-node
- value_split_to_tracks splits vector/color per component
- set_context_position/expanded establish the first entry
- node_get_flags on an empty box returns 0, not u64::MAX
- footage_borrow addrefs its wrapper — fixes double-free

render:
- renderer_create rejects invalid pixel formats (real range check)
- render_frame forwards renderer width/height to the ticket

tests: repro #[ignore]s removed, bug-behavior assertions corrected,
it_undo global-stack tests serialized with a shared lock
This commit is contained in:
2026-08-11 01:30:18 +08:00
parent b564e7a71f
commit aa5fcef66e
13 changed files with 664 additions and 405 deletions
+32 -4
View File
@@ -82,6 +82,11 @@ struct TaskMeta {
/// The project a save task writes (addref'd at creation, released at
/// free) — the module has no save-project getter.
save_project: Option<CHandle>,
/// The project an import task borrows (addref'd at creation, released
/// at free): the module's import task stores its project handle WITHOUT
/// addref, so this facade-side ref keeps the shared box alive while the
/// task runs (see `oakengine_task_create_project_import`).
import_project: Option<CHandle>,
/// The encoding-params box an export task owns, dropped at free
/// (mirrors the C++ `FacadeExportTask` destructor; stored as `usize` so
/// the map stays `Send`).
@@ -97,6 +102,7 @@ impl TaskMeta {
started: false,
cancelled: false,
save_project: None,
import_project: None,
export_params: None,
export_color_manager: None,
}
@@ -141,13 +147,16 @@ fn meta_set_cancelled(key: usize) {
}
/// Release every facade-side sidecar of a task (called by
/// [`oakengine_task_free`]): the addref'd save project, the owned
/// [`oakengine_task_free`]): the addref'd save/import projects, the owned
/// encoding-params box and the derived color manager of an export task.
fn drop_task_meta(key: usize) {
if let Some(meta) = meta_lock().remove(&key) {
if let Some(mut project) = meta.save_project {
unsafe { n::oaknode_project_free(&mut project) };
}
if let Some(mut project) = meta.import_project {
unsafe { n::oaknode_project_free(&mut project) };
}
if let Some(ptr) = meta.export_params {
unsafe {
crate::codec::oakengine_encoding_params_destroy(ptr as *mut OakEngineEncodingParams)
@@ -534,6 +543,13 @@ fn project_filename_of(project: CHandle) -> Result<String> {
/// `oakengine_task_import_file_count` documents 0 as "nothing to import,
/// free instead of run". `url_count < 0`, a NULL URL inside the array, or a
/// folder with no project yield NULL.
///
/// The module's import task stores the project handle WITHOUT addref, so
/// the facade keeps an addref'd copy in [`TaskMeta::import_project`]
/// (released at free) — without it, releasing the transient borrowed
/// handle here would drop the shared box while the task still references
/// it, and the run would read freed memory (the former SIGSEGV reproduced
/// by `it_task::import_run_single_file`).
#[no_mangle]
pub unsafe extern "C" fn oakengine_task_create_project_import(
folder: *mut OakEngineNode,
@@ -551,10 +567,22 @@ pub unsafe extern "C" fn oakengine_task_create_project_import(
return Ok(std::ptr::null_mut());
}
let h = t::oaktask_create_project_import(fh, project, urls, url_count);
// Release the transient borrowed project handle (the import task
// keeps its own copy).
if h.is_null() {
// Creation failed: release the transient borrowed handle.
n::oaknode_project_free(&mut project);
return Ok(std::ptr::null_mut());
}
// Keep the project borrowed for the task's lifetime (the module's
// import task stores the handle without addref): addref before the
// transient handle below is released, so the shared box stays alive
// until `oakengine_task_free` drops the meta-side copy.
let mut meta = TaskMeta::new();
meta.import_project = Some(project.addref());
// Release the transient borrowed project handle (the task's copy and
// the facade-side addref above keep the box alive).
n::oaknode_project_free(&mut project);
Ok(box_task(h))
meta_insert(h.ctx as usize, meta);
Ok(box_handle::<OakEngineTask>(h))
})
}