Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/csg/CsgEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1209,16 +1209,34 @@ CsgNodePtr CsgEvaluator::evalExtrusion(const ExtrusionNode& e, const glm::mat4&
if (name == "scale") {
Value sv = m_interp->evaluate(*exprPtr);
if (sv.isNumber()) {
ext.params["scale_x"] = sv.asNumber();
ext.params["scale_y"] = sv.asNumber();
// A non-finite scale (1/0, -1/0, 0/0) is a real IEEE-754
// Number per evaluate()'s arithmetic propagation, so it'd
// sail past this isNumber() check — but unlike the generic
// param path below (which routes through evalNumber() and
// folds non-finite to 0.0), this branch used to assign the
// raw value straight into scale_x/scale_y. Manifold::Extrude
// then lerps every extruded vertex toward that inf/nan
// scale, producing corrupt geometry that free()s garbage
// heap metadata downstream (issue #105). Leaving scale_x/
// scale_y unset here falls back to MeshEvaluator's default
// 1.0 (no scaling), same as any other malformed scale value.
double s = sv.asNumber();
if (std::isfinite(s)) {
ext.params["scale_x"] = s;
ext.params["scale_y"] = s;
}
} else if (sv.isVector() && sv.asVec().size() == 2) {
// Exactly 2 elements — anything else (e.g. a stray 3-vector)
// isn't a valid linear_extrude() scale and OpenSCAD ignores
// it entirely (falls back to no scaling), rather than using
// just the first two components — confirmed against real
// OpenSCAD's STL output (docs/roadmap.md v3.9).
ext.params["scale_x"] = sv.asVec()[0].asNumber();
ext.params["scale_y"] = sv.asVec()[1].asNumber();
double sx = sv.asVec()[0].asNumber();
double sy = sv.asVec()[1].asNumber();
if (std::isfinite(sx) && std::isfinite(sy)) {
ext.params["scale_x"] = sx;
ext.params["scale_y"] = sy;
}
}
} else if (name == "center") {
Value cv = m_interp->evaluate(*exprPtr);
Expand Down
48 changes: 48 additions & 0 deletions tests/test_csg_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,54 @@ TEST_CASE("CsgEval:linear_extrude scale must be exactly a 2-vector, else no scal
REQUIRE(good.params.at("scale_y") == Approx(5.0));
}

TEST_CASE("CsgEval:linear_extrude non-finite scale is rejected, not passed through",
"[csg][v105][bugfix]") {
// Issue #105: linear_extrude-parameter-tests.scad crashed with heap
// corruption ("free(): invalid next size (fast)"). Root cause: scale=
// is a real IEEE-754 Number for 1/0, -1/0, and 0/0 (evaluate()'s
// arithmetic propagates non-finite doubles), so it passed the
// sv.isNumber() check here — but unlike the generic param path (which
// routes through evalNumber() and folds non-finite to 0.0) and unlike
// "angle" (which is explicitly resolved to a finite fallback), this
// branch used to assign the raw inf/nan straight into scale_x/scale_y.
// MeshEvaluator/Manifold::Extrude then lerped every extruded vertex
// toward that non-finite scale, corrupting the heap. Non-finite scale
// must leave scale_x/scale_y unset (MeshEvaluator's default: no
// scaling), same as any other malformed scale value.
auto posInf = asExtrusion(
evaluate("linear_extrude(height=20, scale=1/0) square(10);").roots[0]);
REQUIRE_FALSE(posInf.params.count("scale_x"));
REQUIRE_FALSE(posInf.params.count("scale_y"));

auto negInf = asExtrusion(
evaluate("linear_extrude(height=20, scale=-1/0) square(10);").roots[0]);
REQUIRE_FALSE(negInf.params.count("scale_x"));
REQUIRE_FALSE(negInf.params.count("scale_y"));

auto nan = asExtrusion(
evaluate("linear_extrude(height=20, scale=0/0) square(10);").roots[0]);
REQUIRE_FALSE(nan.params.count("scale_x"));
REQUIRE_FALSE(nan.params.count("scale_y"));

// Same for a 2-vector with a non-finite component.
auto vecInf = asExtrusion(
evaluate("linear_extrude(height=20, scale=[1/0, 2]) square(10);").roots[0]);
REQUIRE_FALSE(vecInf.params.count("scale_x"));
REQUIRE_FALSE(vecInf.params.count("scale_y"));

// Symmetric case: non-finite in the second component only.
auto vecInf2 = asExtrusion(
evaluate("linear_extrude(height=20, scale=[2, 1/0]) square(10);").roots[0]);
REQUIRE_FALSE(vecInf2.params.count("scale_x"));
REQUIRE_FALSE(vecInf2.params.count("scale_y"));

// A finite scale is unaffected.
auto finite = asExtrusion(
evaluate("linear_extrude(height=20, scale=2) square(10);").roots[0]);
REQUIRE(finite.params.at("scale_x") == Approx(2.0));
REQUIRE(finite.params.at("scale_y") == Approx(2.0));
}

TEST_CASE("CsgEval:rotate_extrude angle keeps NaN/Infinity distinct from a literal 0",
"[csg][v87][bugfix]") {
// Real OpenSCAD (Value::getFiniteDouble()) treats a non-finite angle=
Expand Down
Loading