fix: teardown UAF in Track block cache and headless test/render environment

- Track: new InputDisconnectedEvent trims blocks_/block_array_indexes_
  when a block edge is removed outside the Track's own operations
  (block deletion, undo commands detaching a whole track). Previously
  blocks_ kept dangling pointers and Project teardown crashed in
  track_length() (ASan heap-use-after-free in oakengine_timeline_edit /
  oakengine_sync). The persistent array map is intentionally not
  rewritten so undo of remove_track can re-attach the clips; replace_block
  guards the handler with ignore_block_disconnect_
- filefunctions: honor OAK_CONFIG_DIR to redirect the configuration
  root; QStandardPaths ignores XDG_* on macOS, so the engine tests read
  the real user config (a stale 0.1.x file) and failed frame-rate
  assertions. All engine tests now set OAK_CONFIG_DIR to their tmpdir
- init test: chdir to the fixture directory before loading
  project_with_footage.ove so the engine's moved-project footage
  relocation does not rewrite the stored relative filename
- renderworkerpool: do not let the render worker inherit
  QT_QPA_PLATFORM=offscreen from a headless host process; the worker
  needs a real platform GL context and exited immediately otherwise,
  failing oak_cli_transcode
This commit is contained in:
2026-08-01 20:35:13 +08:00
parent 8399d04b44
commit 30853cbcfd
18 changed files with 84 additions and 0 deletions
+33
View File
@@ -511,7 +511,9 @@ void Track::replace_block(Block *old, Block *replace)
int cache_index = blocks_.indexOf(old);
int index_of_old_block = get_array_index_from_cache_index(cache_index);
ignore_block_disconnect_++;
disconnect_edge(old, NodeInput(this, k_block_input, index_of_old_block));
ignore_block_disconnect_--;
connect_edge(replace, NodeInput(this, k_block_input, index_of_old_block));
blocks_.replace(cache_index, replace);
disconnect(old, &Block::length_changed, this, &Track::block_length_changed);
@@ -584,6 +586,37 @@ void Track::InputConnectedEvent(const QString &input, int element, Node *node)
}
}
void Track::InputDisconnectedEvent(const QString &input, int element,
Node *output)
{
Node::InputDisconnectedEvent(input, element, output);
// Keep the block cache consistent when a block edge is removed outside
// the Track's own mutating operations (e.g. the block is being deleted,
// or an undo command is detaching the whole track from the graph).
// Without this, blocks_ keeps a dangling pointer and later readers such
// as track_length() walk into freed memory.
//
// Only the volatile cache is trimmed here; the persistent array map is
// deliberately left alone so that undo can re-attach the blocks from it
// (InputConnectedEvent rebuilds the cache while arraymap_invalid_ is
// set).
if (input == k_block_input && ignore_block_disconnect_ == 0) {
const int index = blocks_.indexOf(static_cast<Block *>(output));
if (index != -1) {
blocks_.removeAt(index);
block_array_indexes_.removeAt(index);
arraymap_invalid_ = true;
Block *previous = (index > 0) ? blocks_.at(index - 1) : nullptr;
Block *next = (index < blocks_.size()) ? blocks_.at(index) : nullptr;
Block::set_previous_next(previous, next);
update_in_out_from(index);
}
}
}
void Track::update_in_out_from(int index)
{
// Find block just before this one to find the last out point
+9
View File
@@ -460,6 +460,8 @@ signals:
protected:
virtual void InputConnectedEvent(const QString &input, int element,
Node *node) override;
virtual void InputDisconnectedEvent(const QString &input, int element,
Node *output) override;
virtual void InputValueChangedEvent(const QString &input,
int element) override;
@@ -501,6 +503,13 @@ private:
bool arraymap_invalid_;
bool ignore_arraymap_set_;
/**
* @brief Nestable guard suppressing the block-cache maintenance in
* InputDisconnectedEvent while the Track itself is rewiring block edges
* (e.g. replace_block), where the cache update is handled explicitly.
*/
int ignore_block_disconnect_ = 0;
private slots:
void block_length_changed();