Skip to content

fix(emboss): crash in slicing prep after embossing all-space text - #12137

Draft
adele-with-a-b wants to merge 1 commit into
bambulab:masterfrom
adele-with-a-b:emboss-pr4/empty-text-mesh-throws
Draft

fix(emboss): crash in slicing prep after embossing all-space text#12137
adele-with-a-b wants to merge 1 commit into
bambulab:masterfrom
adele-with-a-b:emboss-pr4/empty-text-mesh-throws

Conversation

@adele-with-a-b

Copy link
Copy Markdown
Contributor

What breaks

Embossing text that contains only spaces commits a ModelVolume holding an empty mesh. The next background slicing prep then dereferences a null vector data pointer inside Print::apply, taking the application down.

Root cause

GenerateTextJob::process has three early-exit paths that return normally while leaving m_input.m_final_text_mesh empty. finalize() cannot tell that apart from success, so it commits the empty mesh.

Line numbers are against master at 66e405477.

site condition function
EmbossJob.cpp:1404 m_chars_mesh_result.empty() GenerateTextJob::process
EmbossJob.cpp:1935 bbs.empty() generate_mesh_according_points
EmbossJob.cpp:1940 ms.mesh->empty() generate_mesh_according_points

Three things make this fatal rather than merely a no-op:

  1. generate_mesh_according_points calls mesh.clear() on input_info.m_final_text_mesh at EmbossJob.cpp:1922 before either of its two early exits, so both leave the mesh empty rather than untouched.
  2. GenerateTextJob::finalize tests only canceled || eptr (EmbossJob.cpp:1423-1425). It has no emptiness check, so a normal return from process() reads as success and the mesh is committed through create_text_volume() / recreate_model_volume().
  3. Print::applyupdate_volume_bboxestransformed_its_bbox2d then evaluates its.vertices[its.indices.front()(0)] (PrintApply.cpp:587). The guarding assert(! its.indices.empty()) one line above is compiled out in release, so front() reads through a null data pointer.

Trigger

All-space text. create_all_char_mesh clears its out-parameter at EmbossJob.cpp:1284 and only afterwards tests for all-space input, at EmbossJob.cpp:1291-1293:

wxRegEx re("^ +$");
bool    is_all_space = re.Matches(input_text);
if (is_all_space) { return; }

Because the clear precedes the early return, m_chars_mesh_result comes back empty rather than unmodified. m_final_text_mesh is then still default-constructed — InputInfo is a stack local (GLGizmoText.cpp:3336) and generate_mesh_according_points is its only writer — so the volume that reaches slicing prep has an empty indexed_triangle_set.

The fix

The three sites now throw JobException instead of returning. That is what every other failure path in these two functions already does — EmbossJob.cpp:1408, 1411, 1415, 1932, and 1964. The last of those is especially relevant: it throws on exactly this condition (mesh.its.empty()) at the end of generate_mesh_according_points. The three sites being changed are the outliers, not a new convention.

No signature changes, no new translatable strings. Both messages already exist in this file for these conditions — "Font doesn't have any shape for given text." at EmbossJob.cpp:1415 and 1932, and "There is no valid surface for text projection." at EmbossJob.cpp:1004 and 1024.

This is the same shape and scope as ef96c2001 ("FIX:A special font does not generate x characters, causing software crashes"), which fixed the neighbouring instance of this bug class in this file.

Why the sibling job doesn't have this bug

CreateObjectTextJob::process has the identical bare return at EmbossJob.cpp:1978, but it is safe: its finalize() guards m_input.m_position_points.empty() before building any geometry (EmbossJob.cpp:1990-1992). That is the contract GenerateTextJob::finalize is missing, and throwing restores it without touching either signature.

Deliberately out of scope

Two adjacent problems are left alone, because bundling either would make this change much harder to reason about:

Hardening update_volume_bboxes against an empty indexed_triangle_set. That would defend against any source of an empty volume, not just this one, and it is worth doing. But it is shared slicing-core code, and the obvious implementation — dropping empty volumes from the vector it walks — is unsafe: update_volume_bboxes is the sole writer of PrintObjectRegions::cached_volume_ids (PrintApply.cpp:955-959), and the consumer at PrintApply.cpp:687-690 indexes that vector in a loop whose condition has no bounds check. Removing volumes from the walk desynchronises the two and turns a deterministic fault into an out-of-bounds read. Doing it properly needs its own change.

Error reporting for these jobs. This stops the crash but does not tell the user anything. GenerateTextJob::finalize and CreateObjectTextJob::finalize are the only two of seven finalize() methods in this file that don't route through _finalize(); the other five (EmbossJob.cpp:350, 373, 456, 535, 552) call if (!_finalize(canceled, eptr, *m_input.base)) return;, which hands the exception to exception_process() and shows the user a message. These two test canceled || eptr and discard eptr, which is also why the five JobExceptions already present in GenerateTextJob::process are silent today. Rewiring them changes user-visible error reporting and belongs in its own change.

GenerateTextJob::process has three early-exit paths that return normally
while leaving m_input.m_final_text_mesh empty. finalize() then commits
that empty mesh into a ModelVolume, and the next background slicing prep
dereferences a null vector data pointer.

Line numbers below are against master at 66e4054.

The three sites, all of which now throw JobException instead:

  EmbossJob.cpp:1404  m_chars_mesh_result.empty() in process()
  EmbossJob.cpp:1935  bbs.empty() in generate_mesh_according_points()
  EmbossJob.cpp:1940  ms.mesh->empty() in generate_mesh_according_points()

The reachable trigger for the first site is text consisting only of
spaces. create_all_char_mesh() clears its out-parameter at
EmbossJob.cpp:1284 and only then tests for all-space input, at
EmbossJob.cpp:1291-1293:

    wxRegEx re("^ +$");
    bool    is_all_space = re.Matches(input_text);
    if (is_all_space) { return; }

Because the clear precedes that early return, m_chars_mesh_result comes
back empty rather than unmodified, process() takes the bare return at
:1404, and generate_mesh_according_points() never runs. m_final_text_mesh
is consequently still default-constructed: InputInfo is a stack local
(GLGizmoText.cpp:3336) and generate_mesh_according_points() at
EmbossJob.cpp:1922 is its only writer. finalize() then commits that empty
mesh.

Why returning is wrong here specifically:

  - generate_mesh_according_points() calls mesh.clear() on
    input_info.m_final_text_mesh before either of its two early exits, so
    both leave the mesh empty rather than untouched.
  - GenerateTextJob::finalize() bails only on `canceled || eptr`. It has
    no emptiness check, so a normal return from process() is
    indistinguishable from success and the empty mesh is committed via
    create_text_volume() / recreate_model_volume().
  - Print::apply -> update_volume_bboxes -> transformed_its_bbox2d then
    evaluates `its.vertices[its.indices.front()(0)]`. The guarding
    assert(!its.indices.empty()) at PrintApply.cpp:586 is compiled out in
    release, so front() reads through a null data pointer.

Every other failure path in these two functions already throws
JobException (EmbossJob.cpp:1408, 1411, 1415, 1932, 1964) — including the
identical empty-mesh condition at the end of
generate_mesh_according_points, which throws on mesh.its.empty(). These
three sites are the outliers, not the rule.

The sibling CreateObjectTextJob has the same bare return in its process()
(EmbossJob.cpp:1978) but does not have this bug: its finalize() guards
`m_input.m_position_points.empty()` before building geometry. That is the
contract GenerateTextJob::finalize is missing, and throwing restores it
without changing any signature.

Note that this stops the crash but does not by itself tell the user
anything. GenerateTextJob::finalize is one of only two finalize() methods
in this file that do not route through _finalize() -- five others
(UpdateSurfaceVolumeJob, UpdateJob, CreateObjectJob,
CreateSurfaceVolumeJob, CreateVolumeJob at EmbossJob.cpp:350, 373, 456,
535, 552) call `if (!_finalize(canceled, eptr, *m_input.base)) return;`,
which hands the exception to exception_process() and shows the user
create_message(). GenerateTextJob and CreateObjectTextJob instead test
`canceled || eptr` and discard eptr, so a JobException raised in their
process() produces no message at all. That is also why the five
JobExceptions already present in GenerateTextJob::process are silent
today. Rewiring those two finalize() methods is a user-visible change to
error reporting and is left out of this commit deliberately.

No new translatable strings are introduced. Both messages already exist
in this file for the same conditions — "Font doesn't have any shape for
given text." at EmbossJob.cpp:1415 and 1932, and "There is no valid
surface for text projection." at EmbossJob.cpp:1004 and 1024.

Same shape and scope as ef96c20 ("FIX:A special font does not generate
x characters, causing software crashes"), which fixed the neighbouring
instance of this bug class in this file.

Hardening update_volume_bboxes() itself against an empty indexed_triangle_set
is a separate concern and is deliberately not bundled here: that code is
shared slicing-core, and any change there has to keep
PrintObjectRegions::cached_volume_ids in sync with the volumes it is
walking (PrintApply.cpp:687-690 indexes it without a bounds check).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant