yq/pkg/yqlib/operator_slice.go
Jan Dubois 80139ae1cc
Fix panic on negative slice indices that underflow after adjustment (#2646)
sliceArrayOperator adjusts negative indices by adding Content length,
but does not clamp the result. When the absolute value of a negative
index exceeds Content length (e.g. .[-99999:3] on a 3-element array),
the adjusted index remains negative and causes an out-of-bounds access
in the Content slice loop.

Extract the adjust-and-clamp logic into clampSliceIndex and use it for
both index positions.

Reproducer (panics before this fix, returns full array after):

    echo '[a, b, c]' | yq '.[-99999:3]'

Found by OSS-Fuzz via the lima project's FuzzEvaluateExpression target.
https://issues.oss-fuzz.com/issues/438776028

Signed-off-by: Jan Dubois <jan@jandubois.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:27:02 +10:00

75 lines
2.2 KiB
Go

package yqlib
import (
"container/list"
"fmt"
)
func getSliceNumber(d *dataTreeNavigator, context Context, node *CandidateNode, expressionNode *ExpressionNode) (int, error) {
result, err := d.GetMatchingNodes(context.SingleChildContext(node), expressionNode)
if err != nil {
return 0, err
}
if result.MatchingNodes.Len() != 1 {
return 0, fmt.Errorf("expected to find 1 number, got %v instead", result.MatchingNodes.Len())
}
return parseInt(result.MatchingNodes.Front().Value.(*CandidateNode).Value)
}
// clampSliceIndex resolves a possibly-negative slice index against
// length and clamps the result to [0, length].
func clampSliceIndex(index, length int) int {
if index < 0 {
index += length
}
if index < 0 {
return 0
}
if index > length {
return length
}
return index
}
func sliceArrayOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debug("slice array operator!")
log.Debugf("lhs: %v", expressionNode.LHS.Operation.toString())
log.Debugf("rhs: %v", expressionNode.RHS.Operation.toString())
results := list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
lhsNode := el.Value.(*CandidateNode)
firstNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.LHS)
if err != nil {
return Context{}, err
}
relativeFirstNumber := clampSliceIndex(firstNumber, len(lhsNode.Content))
secondNumber, err := getSliceNumber(d, context, lhsNode, expressionNode.RHS)
if err != nil {
return Context{}, err
}
relativeSecondNumber := clampSliceIndex(secondNumber, len(lhsNode.Content))
log.Debugf("calculateIndicesToTraverse: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
var newResults []*CandidateNode
for i := relativeFirstNumber; i < relativeSecondNumber; i++ {
newResults = append(newResults, lhsNode.Content[i])
}
sliceArrayNode := lhsNode.CreateReplacement(SequenceNode, lhsNode.Tag, "")
sliceArrayNode.AddChildren(newResults)
results.PushBack(sliceArrayNode)
}
// result is now the context that has the nodes we need to put back into a sequence.
//what about multiple arrays in the context? I think we need to create an array for each one
return context.ChildContext(results), nil
}