diff --git a/src/csg/CsgEvaluator.cpp b/src/csg/CsgEvaluator.cpp index 2fcc799..1860e6a 100644 --- a/src/csg/CsgEvaluator.cpp +++ b/src/csg/CsgEvaluator.cpp @@ -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); diff --git a/tests/test_csg_evaluator.cpp b/tests/test_csg_evaluator.cpp index e84f622..53a0554 100644 --- a/tests/test_csg_evaluator.cpp +++ b/tests/test_csg_evaluator.cpp @@ -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=