Skip to content

text.Align.Apply()/VAlign.Apply() abort the process on large maxLength (unrecoverable OOM at 1<<40) #414

Description

@xhon-pelushi

Describe the bug

text.Align.Apply() and text.VAlign.Apply() allocate the requested padding up front with no upper bound, so a large maxLength / maxLines takes the process down instead of returning a clamped string. Small negative values are handled correctly — only the large-positive side is unguarded.

Two distinct failure modes, and the second one is the reason I am reporting it:

  • math.MaxIntrecoverable panic, runtime error: makeslice: len out of range
  • 1 << 40unrecoverable fatal error: runtime: out of memory, which recover() cannot catch, so a caller has no way to defend against it

Steps to reproduce

package main

import (
	"fmt"
	"math"

	"github.com/jedib0t/go-pretty/v6/text"
)

func try(name string, f func()) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Printf("PANIC  %-28s -> %v\n", name, r)
			return
		}
		fmt.Printf("ok     %-28s\n", name)
	}()
	f()
}

func main() {
	try("AlignLeft.Apply MaxInt", func() { text.AlignLeft.Apply("hi", math.MaxInt) })
	try("AlignRight.Apply MaxInt", func() { text.AlignRight.Apply("hi", math.MaxInt) })
	try("AlignCenter.Apply MaxInt", func() { text.AlignCenter.Apply("hi", math.MaxInt) })
	try("AlignJustify.Apply MaxInt", func() { text.AlignJustify.Apply("hi", math.MaxInt) })
	try("VAlignTop.Apply MaxInt", func() { text.VAlignTop.Apply([]string{"hi"}, math.MaxInt) })
	try("VAlignMiddle.Apply MaxInt", func() { text.VAlignMiddle.Apply([]string{"hi"}, math.MaxInt) })
	try("VAlignBottom.Apply MaxInt", func() { text.VAlignBottom.Apply([]string{"hi"}, math.MaxInt) })
	try("AlignLeft.Apply 1<<40", func() { text.AlignLeft.Apply("hi", 1<<40) })
	try("AlignLeft.Apply negative", func() { text.AlignLeft.Apply("hi", -5) })
}

Expected behavior

Return the text padded to whatever is actually representable (or return it unchanged), the same way the negative case already does. In no case should a width argument be able to abort the process.

Actual behavior

PANIC  AlignLeft.Apply MaxInt       -> runtime error: makeslice: len out of range
PANIC  AlignRight.Apply MaxInt      -> runtime error: makeslice: len out of range
PANIC  AlignCenter.Apply MaxInt     -> runtime error: makeslice: len out of range
PANIC  AlignJustify.Apply MaxInt    -> runtime error: makeslice: len out of range
PANIC  VAlignTop.Apply MaxInt       -> runtime error: makeslice: len out of range
PANIC  VAlignMiddle.Apply MaxInt    -> runtime error: makeslice: len out of range
PANIC  VAlignBottom.Apply MaxInt    -> runtime error: makeslice: len out of range
fatal error: runtime: out of memory

The run never reaches the last two cases: 1 << 40 kills the process outright. Tested separately, AlignLeft.Apply("hi", -5) correctly returns "hi".

Root cause

Horizontaltext/align.go. Apply() computes padding := maxLength - sLenWoE and hands it to padText(), which preallocates the whole result before writing a byte:

func padText(text string, left int, right int) string {
	if left <= 0 && right <= 0 {
		return text
	}

	var out strings.Builder
	out.Grow(len(text) + left + right)     // align.go:64 — unbounded
	...
}

The left <= 0 && right <= 0 early return is why negatives are safe; there is no matching upper bound, so Grow(math.MaxInt) reaches makeslice.

Verticaltext/valign.go:42:

linesOut := strings.Split(strings.Repeat("\n", maxLines-1), "\n")

VAlign does guard the low side (if maxLines < 1 { return lines }) but not the high side, so maxLines = math.MaxInt asks strings.Repeat for a MaxInt-1 byte string.

Suggested fix

Clamp in padText() — it is the single choke point for all four Align values — for example by capping the grow hint at something sane and letting the write loops produce the rest, or by rejecting padding beyond a maximum representable width. VAlign.Apply() needs the mirror-image guard to the maxLines < 1 check it already has.

Version

github.com/jedib0t/go-pretty/v6 v6.8.3, go1.25.0, linux/amd64.

Note on how this is reachable

I hit this while probing extreme-value handling across terminal-formatting libraries rather than from a production crash, so treat the severity accordingly. It matters most where a width is computed rather than hard-coded — terminal-size arithmetic that underflows, or a column width taken from config or user input — because the 1 << 40 case cannot be contained with recover().

Activity

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions