Skip to content

flexShrink runaway (~8e9) when the sole shrinkable item has an aspectRatio basis; FMA residual slips past the 1e-6f guard from #1974 #2038

Closed

Description

@sebholl

Sibling case to #1665 / #1783. The 1e-6f guard added in #1974 does not cover it: the residual here is 1.9e-6, and it comes from FMA contraction rather than ordinary accumulation error.

Reproduction

Four nodes, public C API, default config:

#include <yoga/Yoga.h>
#include <cstdio>

int main() {
  YGNodeRef root = YGNodeNew();
  YGNodeStyleSetWidth(root, 350);
  YGNodeStyleSetHeight(root, 350);

  YGNodeRef row = YGNodeNew();
  YGNodeStyleSetFlexDirection(row, YGFlexDirectionRow);
  YGNodeInsertChild(root, row, 0);

  YGNodeRef box = YGNodeNew();
  YGNodeStyleSetAspectRatio(box, 22.0f / 24.0f); // basis = 23 * 22/24 = 21.083334
  YGNodeStyleSetFlexShrink(box, 3);
  YGNodeInsertChild(row, box, 0);

  YGNodeRef sibling = YGNodeNew(); // overflows the row; flexShrink stays 0
  YGNodeStyleSetWidth(sibling, 600);
  YGNodeStyleSetHeight(sibling, 23);
  YGNodeInsertChild(row, sibling, 1);

  YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
  printf("box %g x %g\n", YGNodeLayoutGetWidth(box), YGNodeLayoutGetHeight(box));
}
clang++ -std=c++20 -O2 -I. repro.cpp $(find yoga -name '*.cpp') && ./a.out
box
expected 0 x 0 (basis 21.08, line overflows by 271, shrinks to zero)
actual 8290303488 x 9043966976

flexShrink 1 and 2 clamp to 0 correctly; 3, 6, 9, 11, 12, 19, 22, 999 all explode. Which values fail depends on the mantissa of flexShrink * flexBasis, so there is no safe non-zero value to pick.

Reproduces on main @ 667da3c (2026-10-13) and in the Yoga bundled with React Native 0.86.3. Apple clang, arm64. It does not reproduce with -ffp-contract=off, which is also why the website playground (wasm) cannot show it.

Mechanism

Instrumenting the three places totalFlexShrinkScaledFactors is touched (-O2, default -ffp-contract=on):

[calculateFlexLine]              shrink=3 basis=21.083334 product=-63.25      total=-63.25
[distributeFreeSpaceFirstPass]   freeze, boundMainSize=0                   -> total=1.90734863e-06
[distributeFreeSpaceSecondPass]  remainingFreeSpace=-250 total=1.9e-06 guard=0 -> childSize=8.29e+09

The exact product 3 * 21.083334f is 63.2500019, which is a rounding tie; stored as a float it rounds to even, 63.25. calculateFlexLine does total += -shrink * basis and ends up with -63.25 either way. distributeFreeSpaceFirstPass then does

flexLine.layout.totalFlexShrinkScaledFactors -=
    (-currentLineChild->resolveFlexShrink() *
     currentLineChild->getLayout().computedFlexBasis.unwrap());

which the compiler contracts into a single fmadd, using the unrounded product: -63.25 + 63.2500019 = 2^-19 = 1.9e-6. That is just above the 1e-6f absolute guard in the second pass, so the remaining free space is divided by it.

With -ffp-contract=off the same line gives exactly 0 and the guard fires. Since the residual scales with flexShrink * flexBasis, no absolute epsilon is the right check.

Three preconditions, remove any one and it behaves: aspectRatio on the shrinking item (the basis has to be a non-representable product), flexDirection: row, and an overflowing sibling that is not itself shrinkable (with two shrinkable items the totals do not cancel to a residual). Note the third means the repro silently passes under useWebDefaults, where flexShrink defaults to 1.

Fix

#2039 rounds the product to float before the subtraction and replaces the epsilon with an exact count of shrinkable items left on the line, kept in lockstep with the total. The count is the part that stays correct under -ffp-contract=fast, where hoisting alone can still be contracted. Includes the 4-node repro above as a gtest; full suite passes.

Activity

  1. arkivanov commented on Oct 20, 2026

    @arkivanov

    The same issue with the Java wrapper. The proposed fix seems to solve the problem here as well.

    import com.facebook.yoga.YogaConfigFactory
    import com.facebook.yoga.YogaConstants
    import com.facebook.yoga.YogaFlexDirection
    import com.facebook.yoga.YogaNodeFactory
    import org.junit.Assert.assertEquals
    import org.junit.Test
    
    class YGFlexShrinkMinSizeResidueTest {
    
      @Test
      fun test_min_width_and_percent_width_overflow_row() {
        val config = YogaConfigFactory.create()
    
        val root = YogaNodeFactory.create(config)
        root.flexDirection = YogaFlexDirection.ROW
        root.setWidth(101f)
        root.setHeight(100f)
    
        // flexBasis = minWidth = 224, should not shrink below it.
        val rootChild0 = YogaNodeFactory.create(config)
        rootChild0.flexShrink = 1f
        rootChild0.setMinWidth(224f)
        root.addChildAt(rootChild0, 0)
    
        // flexBasis = 101 * 33.333% = 33.67, shrinks to 0.
        val rootChild1 = YogaNodeFactory.create(config)
        rootChild1.flexShrink = 1f
        rootChild1.setWidthPercent(100f / 3f)
        root.addChildAt(rootChild1, 1)
    
        root.calculateLayout(YogaConstants.UNDEFINED, YogaConstants.UNDEFINED)
    
        assertEquals(101f, root.layoutWidth, 0.0f)
    
        assertEquals(0f, rootChild0.layoutX, 0.0f)
        assertEquals(224f, rootChild0.layoutWidth, 0.0f) // Fails with java.lang.AssertionError: expected:<224.0> but was:<3.6112957E9>
    
        assertEquals(224f, rootChild1.layoutX, 0.0f)
        assertEquals(0f, rootChild1.layoutWidth, 0.0f)
      }
    }
  2. added a commit that references this issue on Oct 20, 2026
    aa6ede7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions