Skip to content

Commit 77df211

Browse files
authored
fix(checker): validate sortBy order argument type (#912)
The sortBy and sort functions accept an optional order argument that must be "asc" or "desc". Previously, passing a non-string value would cause a panic due to an unsafe type assertion in the VM. This adds compile-time validation in the type checker and defensive runtime checks in both the VM and builtin implementations to return proper errors instead of panicking. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
1 parent a9ac10f commit 77df211

6 files changed

Lines changed: 48 additions & 16 deletions

File tree

builtin/builtin.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,13 +1000,17 @@ var Builtins = []*Function{
10001000

10011001
var desc bool
10021002
if len(args) == 2 {
1003-
switch args[1].(string) {
1003+
order, ok := args[1].(string)
1004+
if !ok {
1005+
return nil, 0, fmt.Errorf("sort order argument must be a string (got %T)", args[1])
1006+
}
1007+
switch order {
10041008
case "asc":
10051009
desc = false
10061010
case "desc":
10071011
desc = true
10081012
default:
1009-
return nil, 0, fmt.Errorf("invalid order %s, expected asc or desc", args[1])
1013+
return nil, 0, fmt.Errorf("invalid order %s, expected asc or desc", order)
10101014
}
10111015
}
10121016

checker/checker.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ import (
1515
)
1616

1717
var (
18-
anyType = reflect.TypeOf(new(any)).Elem()
19-
boolType = reflect.TypeOf(true)
20-
intType = reflect.TypeOf(0)
21-
floatType = reflect.TypeOf(float64(0))
22-
stringType = reflect.TypeOf("")
23-
arrayType = reflect.TypeOf([]any{})
24-
mapType = reflect.TypeOf(map[string]any{})
25-
timeType = reflect.TypeOf(time.Time{})
26-
durationType = reflect.TypeOf(time.Duration(0))
18+
anyType = reflect.TypeOf(new(any)).Elem()
19+
boolType = reflect.TypeOf(true)
20+
intType = reflect.TypeOf(0)
21+
floatType = reflect.TypeOf(float64(0))
22+
stringType = reflect.TypeOf("")
23+
arrayType = reflect.TypeOf([]any{})
24+
mapType = reflect.TypeOf(map[string]any{})
25+
timeType = reflect.TypeOf(time.Time{})
26+
durationType = reflect.TypeOf(time.Duration(0))
2727
byteSliceType = reflect.TypeOf([]byte(nil))
2828

2929
anyTypeSlice = []reflect.Type{anyType}
@@ -893,7 +893,10 @@ func (v *Checker) builtinNode(node *ast.BuiltinNode) Nature {
893893
v.end()
894894

895895
if len(node.Arguments) == 3 {
896-
_ = v.visit(node.Arguments[2])
896+
order := v.visit(node.Arguments[2])
897+
if !order.IsString() && !order.IsUnknown(&v.config.NtCache) {
898+
return v.error(node.Arguments[2], "sortBy order argument must be a string (got %v)", order.String())
899+
}
897900
}
898901

899902
if predicate.IsFunc() &&

test/fuzz/fuzz_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ func FuzzExpr(f *testing.F) {
5757
regexp.MustCompile(`reduce of empty array with no initial value`),
5858
regexp.MustCompile(`cannot use <nil> as argument \(type .*\) to call .*`),
5959
regexp.MustCompile(`illegal base64 data at input byte .*`),
60+
regexp.MustCompile(`sort order argument must be a string`),
61+
regexp.MustCompile(`sortBy order argument must be a string`),
62+
regexp.MustCompile(`invalid order .*, expected asc or desc`),
63+
regexp.MustCompile(`unknown order, use asc or desc`),
6064
}
6165

6266
env := NewEnv()

testdata/generated.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,6 @@ $env | reduce(false, $env); list
10041004
$env | reduce(greet, array) ?? foo
10051005
$env | reduce(ok, 1) ?: greet
10061006
$env | reduce(str, foo) not startsWith str
1007-
$env | sortBy(#, 0) * $env && false
10081007
$env | sum(#) not endsWith $env or true
10091008
$env | sum(1.0) <= i
10101009
$env | sum(1.0) in array
@@ -15360,7 +15359,6 @@ f64 ?? reduce(list, str)
1536015359
f64 ?? reverse($env)
1536115360
f64 ?? reverse(array)
1536215361
f64 ?? round(i)
15363-
f64 ?? sortBy($env, #.String, list)
1536415362
f64 ?? sortBy(list, ok)
1536515363
f64 ?? str
1536615364
f64 ?? str[$env:i]
@@ -30051,7 +30049,6 @@ ok || ok || $env
3005130049
ok || one($env, .add)
3005230050
ok || reduce($env, .foo)
3005330051
ok || sortBy($env, array).str
30054-
ok || sortBy($env, foo, 1)
3005530052
ok || sortBy($env, i, str)
3005630053
ok || str != $env
3005730054
ok || str != nil

vm/vm.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,11 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
556556
case 2:
557557
scope := vm.scope()
558558
var desc bool
559-
switch vm.pop().(string) {
559+
order, ok := vm.pop().(string)
560+
if !ok {
561+
panic("sortBy order argument must be a string")
562+
}
563+
switch order {
560564
case "asc":
561565
desc = false
562566
case "desc":

vm/vm_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,26 @@ func TestVM_GroupAndSortOperations(t *testing.T) {
513513
}
514514
}
515515

516+
// TestVM_SortBy_NonStringOrder tests that sortBy with non-string order
517+
// returns a proper error instead of panicking (regression test for OSS-Fuzz #477658245).
518+
func TestVM_SortBy_NonStringOrder(t *testing.T) {
519+
env := map[string]any{}
520+
fn := expr.Function("fn", func(params ...any) (any, error) {
521+
return fmt.Sprintf("fn(%v)", params), nil
522+
})
523+
524+
// This expression passes a function result as the order argument to sortBy.
525+
// The function returns a string that is not "asc" or "desc", which should
526+
// produce a proper error rather than a panic.
527+
program, err := expr.Compile(`sortBy([1, 2, 3], #, fn($env))`, expr.Env(env), fn)
528+
require.NoError(t, err)
529+
530+
testVM := &vm.VM{}
531+
_, err = testVM.Run(program, env)
532+
require.Error(t, err)
533+
require.Contains(t, err.Error(), "unknown order")
534+
}
535+
516536
// TestVM_ProfileOperations tests the profiling opcodes
517537
func TestVM_ProfileOperations(t *testing.T) {
518538
program := &vm.Program{

0 commit comments

Comments
 (0)