[ https://issues.apache.org/jira/browse/COMMONSSITE-177?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]
Gary D. Gregory deleted COMMONSSITE-177: ---------------------------------------- > [Apache Commons CLI] HelpFormatter infinite loop when width=0 > ------------------------------------------------------------- > > Key: COMMONSSITE-177 > URL: https://issues.apache.org/jira/browse/COMMONSSITE-177 > Project: Apache Commons All > Issue Type: Bug > Reporter: Ruiqi Dong > Priority: Critical > Original Estimate: 0.5h > Remaining Estimate: 0.5h > > I am writing to report an infinite loop bug in the HelpFormatter class when > the width parameter is set to 0. Despite existing protective code meant to > prevent this issue, the infinite loop still occurs under certain conditions. > > *Problem Description:* > When calling HelpFormatter.printOptions() or any method that uses > appendWrappedText() with width=0, the application enters an infinite loop, > eventually causing heap space exhaustion. > > > *Test Cases:* > public class HelpFormatterTest { > @Test > public void {*}shouldHandleZeroWidthGracefully{*}() { > HelpFormatter formatter = new HelpFormatter(); > Options options = new Options(); > options.addOption("h", "help", false, "Show help"); > StringWriter out = new StringWriter(); > PrintWriter pw = new PrintWriter(out); > _// execute - this will trigger an infinite loop, but will be interrupted by > @Timeout annotation_ > try { > formatter.printOptions(pw, 0, options, 1, 3); > _// if we get here, it means there was no infinite loop (expected behavior > after fix)_ > String result = out.toString(); > assertNotNull(result, "Result should not be null after fix"); > } catch (Throwable {_}t{_}) { > _// before the fix, this would catch the timeout exception_ > fail("printOptions with width=0 should not cause infinite loop or throw > exception"); > } > } > @Test > public void {*}demonstrateInfiniteLoopMechanism{*}() { > HelpFormatter formatter = new HelpFormatter(); > _// directly test findWrapPos method to demonstrate the root cause_ > String text = "Hello World"; > int pos = formatter.findWrapPos(text, 0, 0); > _// verify: when width=0, findWrapPos returns 0 (for non-empty text)_ > assertEquals(0, pos, "findWrapPos with width=0 returns 0, causing infinite > loop"); > _// this means the text will never be consumed, because substring(0) returns > the entire string_ > } > } > > > *Test Results:* > [{*}INFO{*}] Running org.apache.commons.cli.{*}HelpFormatterTest{*} > [{*}ERROR{*}] {*}Tests run: 2{*}, {*}Failures: 1{*}, Errors: 0, Skipped: 0, > Time elapsed: 25.60 s *<<< FAILURE!* -- in > org.apache.commons.cli.{*}HelpFormatterTest{*} > [{*}ERROR{*}] > org.apache.commons.cli.HelpFormatterTest.shouldHandleZeroWidthGracefully -- > Time elapsed: 25.60 s <<< FAILURE! > org.opentest4j.AssertionFailedError: printOptions with width=0 should not > cause infinite loop or throw exception > at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38) > at org.junit.jupiter.api.Assertions.fail(Assertions.java:138) > at > org.apache.commons.cli.HelpFormatterTest.shouldHandleZeroWidthGracefully(HelpFormatterTest.java:54) > at java.lang.reflect.Method.invoke(Method.java:498) > at java.util.ArrayList.forEach(ArrayList.java:1259) > at java.util.ArrayList.forEach(ArrayList.java:1259) > > > *Root Cause Analysis:* > I've identified the exact mechanism causing the infinite loop: > 1. `findWrapPos(text, 0, 0)` returns 0 for non-empty text (confirmed by my > test) > 2. In `appendWrappedText()` at line 504: `pos = findWrapPos(render, width, > 0);` > 3. When pos=0, line 526: `appendable.append(rtrim(render.substring(0, pos)))` > appends empty string > 4. Line 509 adds a newline > 5. Lines 510-512: The protective code sets `nextLineTabStopPos = 1` when > `nextLineTabStopPos >= width` > 6. However, line 517: `render = padding + render.substring(pos).trim();` > - Since pos=0, `render.substring(0)` returns the entire string > - The text is never consumed, so the loop continues infinitely > > The protective code (lines 510-513) doesn't help because: > - It only adjusts `nextLineTabStopPos` > - It doesn't address that `pos` remains 0, causing text to never be consumed > - Each iteration adds a newline and creates new String objects, eventually > causing heap exhaustion > > > > *Current Code (HelpFormatter.java, lines 501-528):* > <A extends Appendable> A appendWrappedText(final A appendable, final int > width, final int nextLineTabStop, final String text) throws IOException { > String render = text; > int nextLineTabStopPos = nextLineTabStop; > int pos = findWrapPos(render, width, 0); > if (pos == -1) { > appendable.append(rtrim(render)); > return appendable; > } > appendable.append(rtrim(render.substring(0, > pos))).append(getNewLine()); > if (nextLineTabStopPos >= width) { > // stops infinite loop happening > nextLineTabStopPos = 1; > } > // all following lines must be padded with nextLineTabStop space > characters > final String padding = createPadding(nextLineTabStopPos); > while (true) { > render = padding + render.substring(pos).trim(); > pos = findWrapPos(render, width, 0); > if (pos == -1) { > appendable.append(render); > return appendable; > } > if (render.length() > width && pos == nextLineTabStopPos - 1) { > pos = width; > } > appendable.append(rtrim(render.substring(0, > pos))).append(getNewLine()); > } > } > > > *Why the Existing Protection Fails:* > The comment "stops infinite loop happening" suggests awareness of this issue, > but the fix only addresses `nextLineTabStopPos`, not the core problem that > `findWrapPos` returns 0 when width=0, preventing text consumption. > > > *Proposed Solutions:* > 1. Add width validation (recommended): > <A extends Appendable> A appendWrappedText(final A appendable, final int > width, > final int nextLineTabStop, final > String text) throws IOException { > if (width <= 0) { > throw new IllegalArgumentException("Width must be positive, was: " + > width); > } > // existing code... > } > > 2. Handle width=0 as special case: > if (width <= 0) { > appendable.append(text); > return appendable; > } > > 3. Ensure minimum effective width: > int effectiveWidth = Math.max(width, 1); > int pos = findWrapPos(render, effectiveWidth, 0); > > *Impact:* > - Any application passing width=0 to HelpFormatter will experience heap > exhaustion > - Current protective code is insufficient for the width=0 edge case -- This message was sent by Atlassian Jira (v8.20.10#820010)