VYPR
High severity7.5NVD Advisory· Published Mar 17, 2025· Updated Apr 15, 2026

CVE-2025-29786

CVE-2025-29786

Description

Expr is an expression language and expression evaluation for Go. Prior to version 1.17.0, if the Expr expression parser is given an unbounded input string, it will attempt to compile the entire string and generate an Abstract Syntax Tree (AST) node for each part of the expression. In scenarios where input size isn’t limited, a malicious or inadvertent extremely large expression can consume excessive memory as the parser builds a huge AST. This can ultimately lead to*excessive memory usage and an Out-Of-Memory (OOM) crash of the process. This issue is relatively uncommon and will only manifest when there are no restrictions on the input size, i.e. the expression length is allowed to grow arbitrarily large. In typical use cases where inputs are bounded or validated, this problem would not occur. The problem has been patched in the latest versions of the Expr library. The fix introduces compile-time limits on the number of AST nodes and memory usage during parsing, preventing any single expression from exhausting resources. Users should upgrade to Expr version 1.17.0 or later, as this release includes the new node budget and memory limit safeguards. Upgrading to v1.17.0 ensures that extremely deep or large expressions are detected and safely aborted during compilation, avoiding the OOM condition. For users who cannot immediately upgrade, the recommended workaround is to impose an input size restriction before parsing. In practice, this means validating or limiting the length of expression strings that your application will accept. For example, set a maximum allowable number of characters (or nodes) for any expression and reject or truncate inputs that exceed this limit. By ensuring no unbounded-length expression is ever fed into the parser, one can prevent the parser from constructing a pathologically large AST and avoid potential memory exhaustion. In short, pre-validate and cap input size as a safeguard in the absence of the patch.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/expr-lang/exprGo
< 1.17.01.17.0

Patches

2
0d1944145442

feat: add node budget and memory limits (#762)

https://github.com/expr-lang/exprVille VesilehtoMar 7, 2025via ghsa
6 files changed · +452 94
  • conf/config.go+29 17 modified
    @@ -10,31 +10,43 @@ import (
     	"github.com/expr-lang/expr/vm/runtime"
     )
     
    +const (
    +	// DefaultMemoryBudget represents an upper limit of memory usage
    +	DefaultMemoryBudget uint = 1e6
    +
    +	// DefaultMaxNodes represents an upper limit of AST nodes
    +	DefaultMaxNodes uint = 10000
    +)
    +
     type FunctionsTable map[string]*builtin.Function
     
     type Config struct {
    -	EnvObject any
    -	Env       nature.Nature
    -	Expect    reflect.Kind
    -	ExpectAny bool
    -	Optimize  bool
    -	Strict    bool
    -	Profile   bool
    -	ConstFns  map[string]reflect.Value
    -	Visitors  []ast.Visitor
    -	Functions FunctionsTable
    -	Builtins  FunctionsTable
    -	Disabled  map[string]bool // disabled builtins
    +	EnvObject    any
    +	Env          nature.Nature
    +	Expect       reflect.Kind
    +	ExpectAny    bool
    +	Optimize     bool
    +	Strict       bool
    +	Profile      bool
    +	MaxNodes     uint
    +	MemoryBudget uint
    +	ConstFns     map[string]reflect.Value
    +	Visitors     []ast.Visitor
    +	Functions    FunctionsTable
    +	Builtins     FunctionsTable
    +	Disabled     map[string]bool // disabled builtins
     }
     
     // CreateNew creates new config with default values.
     func CreateNew() *Config {
     	c := &Config{
    -		Optimize:  true,
    -		ConstFns:  make(map[string]reflect.Value),
    -		Functions: make(map[string]*builtin.Function),
    -		Builtins:  make(map[string]*builtin.Function),
    -		Disabled:  make(map[string]bool),
    +		Optimize:     true,
    +		MaxNodes:     DefaultMaxNodes,
    +		MemoryBudget: DefaultMemoryBudget,
    +		ConstFns:     make(map[string]reflect.Value),
    +		Functions:    make(map[string]*builtin.Function),
    +		Builtins:     make(map[string]*builtin.Function),
    +		Disabled:     make(map[string]bool),
     	}
     	for _, f := range builtin.Builtins {
     		c.Builtins[f.Name] = f
    
  • parser/parser.go+166 68 modified
    @@ -45,12 +45,47 @@ var predicates = map[string]struct {
     }
     
     type parser struct {
    -	tokens  []Token
    -	current Token
    -	pos     int
    -	err     *file.Error
    -	depth   int // predicate call depth
    -	config  *conf.Config
    +	tokens    []Token
    +	current   Token
    +	pos       int
    +	err       *file.Error
    +	depth     int // predicate call depth
    +	config    *conf.Config
    +	nodeCount uint // tracks number of AST nodes created
    +}
    +
    +// checkNodeLimit verifies that adding a new node won't exceed configured limits
    +func (p *parser) checkNodeLimit() error {
    +	p.nodeCount++
    +	if p.config.MaxNodes > 0 && p.nodeCount > p.config.MaxNodes {
    +		p.error("compilation failed: expression exceeds maximum allowed nodes")
    +		return nil
    +	}
    +	return nil
    +}
    +
    +// createNode handles creation of regular nodes
    +func (p *parser) createNode(n Node, loc file.Location) Node {
    +	if err := p.checkNodeLimit(); err != nil {
    +		return nil
    +	}
    +	if n == nil || p.err != nil {
    +		return nil
    +	}
    +	n.SetLocation(loc)
    +	return n
    +}
    +
    +// createMemberNode handles creation of member nodes
    +func (p *parser) createMemberNode(n *MemberNode, loc file.Location) *MemberNode {
    +	if err := p.checkNodeLimit(); err != nil {
    +		return nil
    +	}
    +	if n == nil || p.err != nil {
    +		return nil
    +	}
    +	n.SetLocation(loc)
    +	return n
     }
     
     type Tree struct {
    @@ -129,6 +164,10 @@ func (p *parser) expect(kind Kind, values ...string) {
     // parse functions
     
     func (p *parser) parseExpression(precedence int) Node {
    +	if p.err != nil {
    +		return nil
    +	}
    +
     	if precedence == 0 && p.current.Is(Operator, "let") {
     		return p.parseVariableDeclaration()
     	}
    @@ -190,19 +229,23 @@ func (p *parser) parseExpression(precedence int) Node {
     				nodeRight = p.parseExpression(op.Precedence)
     			}
     
    -			nodeLeft = &BinaryNode{
    +			nodeLeft = p.createNode(&BinaryNode{
     				Operator: opToken.Value,
     				Left:     nodeLeft,
     				Right:    nodeRight,
    +			}, opToken.Location)
    +			if nodeLeft == nil {
    +				return nil
     			}
    -			nodeLeft.SetLocation(opToken.Location)
     
     			if negate {
    -				nodeLeft = &UnaryNode{
    +				nodeLeft = p.createNode(&UnaryNode{
     					Operator: "not",
     					Node:     nodeLeft,
    +				}, notToken.Location)
    +				if nodeLeft == nil {
    +					return nil
     				}
    -				nodeLeft.SetLocation(notToken.Location)
     			}
     
     			goto next
    @@ -229,13 +272,11 @@ func (p *parser) parseVariableDeclaration() Node {
     	value := p.parseExpression(0)
     	p.expect(Operator, ";")
     	node := p.parseExpression(0)
    -	let := &VariableDeclaratorNode{
    +	return p.createNode(&VariableDeclaratorNode{
     		Name:  variableName.Value,
     		Value: value,
     		Expr:  node,
    -	}
    -	let.SetLocation(variableName.Location)
    -	return let
    +	}, variableName.Location)
     }
     
     func (p *parser) parseConditionalIf() Node {
    @@ -272,10 +313,13 @@ func (p *parser) parseConditional(node Node) Node {
     			expr2 = p.parseExpression(0)
     		}
     
    -		node = &ConditionalNode{
    +		node = p.createNode(&ConditionalNode{
     			Cond: node,
     			Exp1: expr1,
     			Exp2: expr2,
    +		}, p.current.Location)
    +		if node == nil {
    +			return nil
     		}
     	}
     	return node
    @@ -288,11 +332,13 @@ func (p *parser) parsePrimary() Node {
     		if op, ok := operator.Unary[token.Value]; ok {
     			p.next()
     			expr := p.parseExpression(op.Precedence)
    -			node := &UnaryNode{
    +			node := p.createNode(&UnaryNode{
     				Operator: token.Value,
     				Node:     expr,
    +			}, token.Location)
    +			if node == nil {
    +				return nil
     			}
    -			node.SetLocation(token.Location)
     			return p.parsePostfixExpression(node)
     		}
     	}
    @@ -314,8 +360,10 @@ func (p *parser) parsePrimary() Node {
     					p.next()
     				}
     			}
    -			node := &PointerNode{Name: name}
    -			node.SetLocation(token.Location)
    +			node := p.createNode(&PointerNode{Name: name}, token.Location)
    +			if node == nil {
    +				return nil
    +			}
     			return p.parsePostfixExpression(node)
     		}
     	} else {
    @@ -344,23 +392,31 @@ func (p *parser) parseSecondary() Node {
     		p.next()
     		switch token.Value {
     		case "true":
    -			node := &BoolNode{Value: true}
    -			node.SetLocation(token.Location)
    +			node = p.createNode(&BoolNode{Value: true}, token.Location)
    +			if node == nil {
    +				return nil
    +			}
     			return node
     		case "false":
    -			node := &BoolNode{Value: false}
    -			node.SetLocation(token.Location)
    +			node = p.createNode(&BoolNode{Value: false}, token.Location)
    +			if node == nil {
    +				return nil
    +			}
     			return node
     		case "nil":
    -			node := &NilNode{}
    -			node.SetLocation(token.Location)
    +			node = p.createNode(&NilNode{}, token.Location)
    +			if node == nil {
    +				return nil
    +			}
     			return node
     		default:
     			if p.current.Is(Bracket, "(") {
     				node = p.parseCall(token, []Node{}, true)
     			} else {
    -				node = &IdentifierNode{Value: token.Value}
    -				node.SetLocation(token.Location)
    +				node = p.createNode(&IdentifierNode{Value: token.Value}, token.Location)
    +				if node == nil {
    +					return nil
    +				}
     			}
     		}
     
    @@ -407,8 +463,10 @@ func (p *parser) parseSecondary() Node {
     		return node
     	case String:
     		p.next()
    -		node = &StringNode{Value: token.Value}
    -		node.SetLocation(token.Location)
    +		node = p.createNode(&StringNode{Value: token.Value}, token.Location)
    +		if node == nil {
    +			return nil
    +		}
     
     	default:
     		if token.Is(Bracket, "[") {
    @@ -428,15 +486,15 @@ func (p *parser) toIntegerNode(number int64) Node {
     		p.error("integer literal is too large")
     		return nil
     	}
    -	return &IntegerNode{Value: int(number)}
    +	return p.createNode(&IntegerNode{Value: int(number)}, p.current.Location)
     }
     
     func (p *parser) toFloatNode(number float64) Node {
     	if number > math.MaxFloat64 {
     		p.error("float literal is too large")
     		return nil
     	}
    -	return &FloatNode{Value: number}
    +	return p.createNode(&FloatNode{Value: number}, p.current.Location)
     }
     
     func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) Node {
    @@ -478,25 +536,34 @@ func (p *parser) parseCall(token Token, arguments []Node, checkOverrides bool) N
     
     		p.expect(Bracket, ")")
     
    -		node = &BuiltinNode{
    +		node = p.createNode(&BuiltinNode{
     			Name:      token.Value,
     			Arguments: arguments,
    +		}, token.Location)
    +		if node == nil {
    +			return nil
     		}
    -		node.SetLocation(token.Location)
     	} else if _, ok := builtin.Index[token.Value]; ok && !p.config.Disabled[token.Value] && !isOverridden {
    -		node = &BuiltinNode{
    +		node = p.createNode(&BuiltinNode{
     			Name:      token.Value,
     			Arguments: p.parseArguments(arguments),
    +		}, token.Location)
    +		if node == nil {
    +			return nil
     		}
    -		node.SetLocation(token.Location)
    +
     	} else {
    -		callee := &IdentifierNode{Value: token.Value}
    -		callee.SetLocation(token.Location)
    -		node = &CallNode{
    +		callee := p.createNode(&IdentifierNode{Value: token.Value}, token.Location)
    +		if callee == nil {
    +			return nil
    +		}
    +		node = p.createNode(&CallNode{
     			Callee:    callee,
     			Arguments: p.parseArguments(arguments),
    +		}, token.Location)
    +		if node == nil {
    +			return nil
     		}
    -		node.SetLocation(token.Location)
     	}
     	return node
     }
    @@ -534,10 +601,12 @@ func (p *parser) parsePredicate() Node {
     	if expectClosingBracket {
     		p.expect(Bracket, "}")
     	}
    -	predicateNode := &PredicateNode{
    +	predicateNode := p.createNode(&PredicateNode{
     		Node: node,
    +	}, startToken.Location)
    +	if predicateNode == nil {
    +		return nil
     	}
    -	predicateNode.SetLocation(startToken.Location)
     	return predicateNode
     }
     
    @@ -558,8 +627,10 @@ func (p *parser) parseArrayExpression(token Token) Node {
     end:
     	p.expect(Bracket, "]")
     
    -	node := &ArrayNode{Nodes: nodes}
    -	node.SetLocation(token.Location)
    +	node := p.createNode(&ArrayNode{Nodes: nodes}, token.Location)
    +	if node == nil {
    +		return nil
    +	}
     	return node
     }
     
    @@ -585,8 +656,10 @@ func (p *parser) parseMapExpression(token Token) Node {
     		//  * identifier, which is equivalent to a string
     		//  * expression, which must be enclosed in parentheses -- (1 + 2)
     		if p.current.Is(Number) || p.current.Is(String) || p.current.Is(Identifier) {
    -			key = &StringNode{Value: p.current.Value}
    -			key.SetLocation(token.Location)
    +			key = p.createNode(&StringNode{Value: p.current.Value}, p.current.Location)
    +			if key == nil {
    +				return nil
    +			}
     			p.next()
     		} else if p.current.Is(Bracket, "(") {
     			key = p.parseExpression(0)
    @@ -597,16 +670,20 @@ func (p *parser) parseMapExpression(token Token) Node {
     		p.expect(Operator, ":")
     
     		node := p.parseExpression(0)
    -		pair := &PairNode{Key: key, Value: node}
    -		pair.SetLocation(token.Location)
    +		pair := p.createNode(&PairNode{Key: key, Value: node}, token.Location)
    +		if pair == nil {
    +			return nil
    +		}
     		nodes = append(nodes, pair)
     	}
     
     end:
     	p.expect(Bracket, "}")
     
    -	node := &MapNode{Pairs: nodes}
    -	node.SetLocation(token.Location)
    +	node := p.createNode(&MapNode{Pairs: nodes}, token.Location)
    +	if node == nil {
    +		return nil
    +	}
     	return node
     }
     
    @@ -631,8 +708,10 @@ func (p *parser) parsePostfixExpression(node Node) Node {
     				p.error("expected name")
     			}
     
    -			property := &StringNode{Value: propertyToken.Value}
    -			property.SetLocation(propertyToken.Location)
    +			property := p.createNode(&StringNode{Value: propertyToken.Value}, propertyToken.Location)
    +			if property == nil {
    +				return nil
    +			}
     
     			chainNode, isChain := node.(*ChainNode)
     			optional := postfixToken.Value == "?."
    @@ -641,26 +720,33 @@ func (p *parser) parsePostfixExpression(node Node) Node {
     				node = chainNode.Node
     			}
     
    -			memberNode := &MemberNode{
    +			memberNode := p.createMemberNode(&MemberNode{
     				Node:     node,
     				Property: property,
     				Optional: optional,
    +			}, propertyToken.Location)
    +			if memberNode == nil {
    +				return nil
     			}
    -			memberNode.SetLocation(propertyToken.Location)
     
     			if p.current.Is(Bracket, "(") {
     				memberNode.Method = true
    -				node = &CallNode{
    +				node = p.createNode(&CallNode{
     					Callee:    memberNode,
     					Arguments: p.parseArguments([]Node{}),
    +				}, propertyToken.Location)
    +				if node == nil {
    +					return nil
     				}
    -				node.SetLocation(propertyToken.Location)
     			} else {
     				node = memberNode
     			}
     
     			if isChain || optional {
    -				node = &ChainNode{Node: node}
    +				node = p.createNode(&ChainNode{Node: node}, propertyToken.Location)
    +				if node == nil {
    +					return nil
    +				}
     			}
     
     		} else if postfixToken.Value == "[" {
    @@ -674,11 +760,13 @@ func (p *parser) parsePostfixExpression(node Node) Node {
     					to = p.parseExpression(0)
     				}
     
    -				node = &SliceNode{
    +				node = p.createNode(&SliceNode{
     					Node: node,
     					To:   to,
    +				}, postfixToken.Location)
    +				if node == nil {
    +					return nil
     				}
    -				node.SetLocation(postfixToken.Location)
     				p.expect(Bracket, "]")
     
     			} else {
    @@ -692,25 +780,32 @@ func (p *parser) parsePostfixExpression(node Node) Node {
     						to = p.parseExpression(0)
     					}
     
    -					node = &SliceNode{
    +					node = p.createNode(&SliceNode{
     						Node: node,
     						From: from,
     						To:   to,
    +					}, postfixToken.Location)
    +					if node == nil {
    +						return nil
     					}
    -					node.SetLocation(postfixToken.Location)
     					p.expect(Bracket, "]")
     
     				} else {
     					// Slice operator [:] was not found,
     					// it should be just an index node.
    -					node = &MemberNode{
    +					node = p.createNode(&MemberNode{
     						Node:     node,
     						Property: from,
     						Optional: optional,
    +					}, postfixToken.Location)
    +					if node == nil {
    +						return nil
     					}
    -					node.SetLocation(postfixToken.Location)
     					if optional {
    -						node = &ChainNode{Node: node}
    +						node = p.createNode(&ChainNode{Node: node}, postfixToken.Location)
    +						if node == nil {
    +							return nil
    +						}
     					}
     					p.expect(Bracket, "]")
     				}
    @@ -722,26 +817,29 @@ func (p *parser) parsePostfixExpression(node Node) Node {
     	}
     	return node
     }
    -
     func (p *parser) parseComparison(left Node, token Token, precedence int) Node {
     	var rootNode Node
     	for {
     		comparator := p.parseExpression(precedence + 1)
    -		cmpNode := &BinaryNode{
    +		cmpNode := p.createNode(&BinaryNode{
     			Operator: token.Value,
     			Left:     left,
     			Right:    comparator,
    +		}, token.Location)
    +		if cmpNode == nil {
    +			return nil
     		}
    -		cmpNode.SetLocation(token.Location)
     		if rootNode == nil {
     			rootNode = cmpNode
     		} else {
    -			rootNode = &BinaryNode{
    +			rootNode = p.createNode(&BinaryNode{
     				Operator: "&&",
     				Left:     rootNode,
     				Right:    cmpNode,
    +			}, token.Location)
    +			if rootNode == nil {
    +				return nil
     			}
    -			rootNode.SetLocation(token.Location)
     		}
     
     		left = comparator
    
  • parser/parser_test.go+76 0 modified
    @@ -5,6 +5,7 @@ import (
     	"strings"
     	"testing"
     
    +	"github.com/expr-lang/expr/conf"
     	"github.com/expr-lang/expr/internal/testify/assert"
     	"github.com/expr-lang/expr/internal/testify/require"
     
    @@ -925,3 +926,78 @@ func TestParse_pipe_operator(t *testing.T) {
     	require.NoError(t, err)
     	assert.Equal(t, Dump(expect), Dump(actual.Node))
     }
    +
    +func TestNodeBudget(t *testing.T) {
    +	tests := []struct {
    +		name        string
    +		expr        string
    +		maxNodes    uint
    +		shouldError bool
    +	}{
    +		{
    +			name:        "simple expression equal to limit",
    +			expr:        "a + b",
    +			maxNodes:    3,
    +			shouldError: false,
    +		},
    +		{
    +			name:        "medium expression under limit",
    +			expr:        "a + b * c / d",
    +			maxNodes:    20,
    +			shouldError: false,
    +		},
    +		{
    +			name:        "deeply nested expression over limit",
    +			expr:        "1 + (2 + (3 + (4 + (5 + (6 + (7 + 8))))))",
    +			maxNodes:    10,
    +			shouldError: true,
    +		},
    +		{
    +			name:        "array expression over limit",
    +			expr:        "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]",
    +			maxNodes:    5,
    +			shouldError: true,
    +		},
    +		{
    +			name:        "disabled node budget",
    +			expr:        "1 + (2 + (3 + (4 + (5 + (6 + (7 + 8))))))",
    +			maxNodes:    0,
    +			shouldError: false,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			config := conf.CreateNew()
    +			config.MaxNodes = tt.maxNodes
    +			config.Disabled = make(map[string]bool, 0)
    +
    +			_, err := parser.ParseWithConfig(tt.expr, config)
    +			hasError := err != nil && strings.Contains(err.Error(), "exceeds maximum allowed nodes")
    +
    +			if hasError != tt.shouldError {
    +				t.Errorf("ParseWithConfig(%q) error = %v, shouldError %v", tt.expr, err, tt.shouldError)
    +			}
    +
    +			// Verify error message format when expected
    +			if tt.shouldError && err != nil {
    +				expected := "compilation failed: expression exceeds maximum allowed nodes"
    +				if !strings.Contains(err.Error(), expected) {
    +					t.Errorf("Expected error message to contain %q, got %q", expected, err.Error())
    +				}
    +			}
    +		})
    +	}
    +}
    +
    +func TestNodeBudgetDisabled(t *testing.T) {
    +	config := conf.CreateNew()
    +	config.MaxNodes = 0 // Disable node budget
    +
    +	expr := strings.Repeat("a + ", 1000) + "b"
    +	_, err := parser.ParseWithConfig(expr, config)
    +
    +	if err != nil && strings.Contains(err.Error(), "exceeds maximum allowed nodes") {
    +		t.Error("Node budget check should be disabled when MaxNodes is 0")
    +	}
    +}
    
  • vm/utils.go+0 3 modified
    @@ -11,9 +11,6 @@ type (
     )
     
     var (
    -	// MemoryBudget represents an upper limit of memory usage.
    -	MemoryBudget uint = 1e6
    -
     	errorType = reflect.TypeOf((*error)(nil)).Elem()
     )
     
    
  • vm/vm.go+19 4 modified
    @@ -11,6 +11,7 @@ import (
     	"time"
     
     	"github.com/expr-lang/expr/builtin"
    +	"github.com/expr-lang/expr/conf"
     	"github.com/expr-lang/expr/file"
     	"github.com/expr-lang/expr/internal/deref"
     	"github.com/expr-lang/expr/vm/runtime"
    @@ -20,11 +21,23 @@ func Run(program *Program, env any) (any, error) {
     	if program == nil {
     		return nil, fmt.Errorf("program is nil")
     	}
    -
     	vm := VM{}
     	return vm.Run(program, env)
     }
     
    +func RunWithConfig(program *Program, env any, config *conf.Config) (any, error) {
    +	if program == nil {
    +		return nil, fmt.Errorf("program is nil")
    +	}
    +	if config == nil {
    +		return nil, fmt.Errorf("config is nil")
    +	}
    +	vm := VM{
    +		MemoryBudget: config.MemoryBudget,
    +	}
    +	return vm.Run(program, env)
    +}
    +
     func Debug() *VM {
     	vm := &VM{
     		debug: true,
    @@ -38,9 +51,9 @@ type VM struct {
     	Stack        []any
     	Scopes       []*Scope
     	Variables    []any
    +	MemoryBudget uint
     	ip           int
     	memory       uint
    -	memoryBudget uint
     	debug        bool
     	step         chan struct{}
     	curr         chan int
    @@ -76,7 +89,9 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
     		vm.Variables = make([]any, program.variables)
     	}
     
    -	vm.memoryBudget = MemoryBudget
    +	if vm.MemoryBudget == 0 {
    +		vm.MemoryBudget = conf.DefaultMemoryBudget
    +	}
     	vm.memory = 0
     	vm.ip = 0
     
    @@ -599,7 +614,7 @@ func (vm *VM) pop() any {
     
     func (vm *VM) memGrow(size uint) {
     	vm.memory += size
    -	if vm.memory >= vm.memoryBudget {
    +	if vm.memory >= vm.MemoryBudget {
     		panic("memory budget exceeded")
     	}
     }
    
  • vm/vm_test.go+162 2 modified
    @@ -4,6 +4,7 @@ import (
     	"errors"
     	"fmt"
     	"reflect"
    +	"strings"
     	"testing"
     
     	"github.com/expr-lang/expr/internal/testify/require"
    @@ -17,8 +18,35 @@ import (
     )
     
     func TestRun_NilProgram(t *testing.T) {
    -	_, err := vm.Run(nil, nil)
    -	require.Error(t, err)
    +	t.Run("run with nil program", func(t *testing.T) {
    +		newVM, err := vm.Run(nil, nil)
    +		require.Error(t, err)
    +		require.Nil(t, newVM)
    +	})
    +	t.Run("run with nil program and nil config", func(t *testing.T) {
    +		newVM, err := vm.RunWithConfig(nil, nil, nil)
    +		require.Error(t, err)
    +		require.Nil(t, newVM)
    +	})
    +	t.Run("run with nil config", func(t *testing.T) {
    +		program, err := expr.Compile("1")
    +		require.Nil(t, err)
    +		newVM, err := vm.RunWithConfig(program, nil, nil)
    +		require.Error(t, err)
    +		require.Nil(t, newVM)
    +	})
    +	t.Run("run with config", func(t *testing.T) {
    +		program, err := expr.Compile("1")
    +		require.Nil(t, err)
    +		config := conf.New(nil)
    +		env := map[string]any{
    +			"a": 1,
    +		}
    +		config.MemoryBudget = 100
    +		newVM, err := vm.RunWithConfig(program, env, config)
    +		require.Nil(t, err)
    +		require.Equal(t, newVM, 1)
    +	})
     }
     
     func TestRun_ReuseVM(t *testing.T) {
    @@ -1191,3 +1219,135 @@ func TestVM_DirectBasicOpcodes(t *testing.T) {
     		})
     	}
     }
    +
    +func TestVM_MemoryBudget(t *testing.T) {
    +	tests := []struct {
    +		name        string
    +		expr        string
    +		memBudget   uint
    +		expectError string
    +	}{
    +		{
    +			name:      "under budget",
    +			expr:      "map(1..10, #)",
    +			memBudget: 100,
    +		},
    +		{
    +			name:        "exceeds budget",
    +			expr:        "map(1..1000, #)",
    +			memBudget:   10,
    +			expectError: "memory budget exceeded",
    +		},
    +		{
    +			name:      "zero budget uses default",
    +			expr:      "map(1..10, #)",
    +			memBudget: 0,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			node, err := parser.Parse(tt.expr)
    +			require.NoError(t, err)
    +
    +			program, err := compiler.Compile(node, nil)
    +			require.NoError(t, err)
    +
    +			vm := vm.VM{MemoryBudget: tt.memBudget}
    +			out, err := vm.Run(program, nil)
    +
    +			if tt.expectError != "" {
    +				require.Error(t, err)
    +				require.Contains(t, err.Error(), tt.expectError)
    +			} else {
    +				require.NoError(t, err)
    +				require.NotNil(t, out)
    +			}
    +		})
    +	}
    +}
    +
    +// Helper functions for creating deeply nested expressions
    +func createNestedArithmeticExpr(t *testing.T, depth int) string {
    +	t.Helper()
    +	if depth == 0 {
    +		return "a"
    +	}
    +	return fmt.Sprintf("(%s + %d)", createNestedArithmeticExpr(t, depth-1), depth)
    +}
    +
    +func createNestedMapExpr(t *testing.T, depth int) string {
    +	t.Helper()
    +	if depth == 0 {
    +		return `{"value": 1}`
    +	}
    +	return fmt.Sprintf(`{"nested": %s}`, createNestedMapExpr(t, depth-1))
    +}
    +
    +func TestVM_Limits(t *testing.T) {
    +	tests := []struct {
    +		name         string
    +		expr         string
    +		memoryBudget uint
    +		maxNodes     uint
    +		env          map[string]any
    +		expectError  string
    +	}{
    +		{
    +			name:         "nested arithmetic allowed with max nodes and memory budget",
    +			expr:         createNestedArithmeticExpr(t, 100),
    +			env:          map[string]any{"a": 1},
    +			maxNodes:     1000,
    +			memoryBudget: 1, // arithmetic expressions not counted towards memory budget
    +		},
    +		{
    +			name:         "nested arithmetic blocked by max nodes",
    +			expr:         createNestedArithmeticExpr(t, 10000),
    +			env:          map[string]any{"a": 1},
    +			maxNodes:     100,
    +			memoryBudget: 1, // arithmetic expressions not counted towards memory budget
    +			expectError:  "compilation failed: expression exceeds maximum allowed nodes",
    +		},
    +		{
    +			name:         "nested map blocked by memory budget",
    +			expr:         createNestedMapExpr(t, 100),
    +			env:          map[string]any{},
    +			maxNodes:     1000,
    +			memoryBudget: 10, // Small memory budget to trigger limit
    +			expectError:  "memory budget exceeded",
    +		},
    +	}
    +
    +	for _, test := range tests {
    +		t.Run(test.name, func(t *testing.T) {
    +			var options []expr.Option
    +			options = append(options, expr.Env(test.env))
    +			if test.maxNodes > 0 {
    +				options = append(options, func(c *conf.Config) {
    +					c.MaxNodes = test.maxNodes
    +				})
    +			}
    +
    +			program, err := expr.Compile(test.expr, options...)
    +			if err != nil {
    +				if test.expectError != "" && strings.Contains(err.Error(), test.expectError) {
    +					return
    +				}
    +				t.Fatal(err)
    +			}
    +
    +			testVM := &vm.VM{
    +				MemoryBudget: test.memoryBudget,
    +			}
    +
    +			_, err = testVM.Run(program, test.env)
    +
    +			if test.expectError == "" {
    +				require.NoError(t, err)
    +			} else {
    +				require.Error(t, err)
    +				require.Contains(t, err.Error(), test.expectError)
    +			}
    +		})
    +	}
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.