From 9b5d41fd49dba455c5210421ee2312a6e732c065 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 06:41:43 +0000 Subject: [PATCH 1/2] Fix issue #105: linear_extrude scale= heap corruption on non-finite values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CsgEvaluator::evalExtrusion() special-cases "scale" to support both a scalar and a 2-vector, but unlike every other extrusion param (which routes through evalNumber(), folding non-finite doubles to 0.0) and unlike "angle" (which explicitly resolves non-finite to a fallback), the scale branch assigned sv.asNumber() straight into scale_x/scale_y. 1/0, -1/0, and 0/0 are real IEEE-754 Numbers per the interpreter's arithmetic propagation, so they sailed through the isNumber() check unclamped. MeshEvaluator/Manifold::Extrude then lerped every extruded vertex toward that inf/nan scale, corrupting the heap ("free(): invalid next size (fast)") — reproduced with linear_extrude(scale=1/0), matching OpenSCAD's own linear_extrude-parameter-tests.scad corpus file. Now a non-finite scalar or vector component is rejected, leaving scale_x/scale_y unset so MeshEvaluator falls back to its default 1.0 (no scaling) — the same "malformed scale is ignored" behavior already used for a stray 3-element vector. --- src/csg/CsgEvaluator.cpp | 26 ++++++++++++++++++---- tests/test_csg_evaluator.cpp | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) 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..504292f 100644 --- a/tests/test_csg_evaluator.cpp +++ b/tests/test_csg_evaluator.cpp @@ -1560,6 +1560,48 @@ 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")); + + // 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= From 81e8c7c664f7bb869fec616cabf6a9dd7ef63a22 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 07:01:29 +0000 Subject: [PATCH 2/2] Add symmetric non-finite scale=[2, 1/0] test case (PR #107 review nit) Mirrors the existing scale=[1/0, 2] case to also cover a non-finite value in the vector's second component only. --- tests/test_csg_evaluator.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_csg_evaluator.cpp b/tests/test_csg_evaluator.cpp index 504292f..53a0554 100644 --- a/tests/test_csg_evaluator.cpp +++ b/tests/test_csg_evaluator.cpp @@ -1595,6 +1595,12 @@ TEST_CASE("CsgEval:linear_extrude non-finite scale is rejected, not passed throu 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]);