diff --git a/vm/vm.go b/vm/vm.go index 4b459f78..b338ddbc 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -254,6 +254,25 @@ func equal(lhsV, rhsV reflect.Value) bool { return reflect.DeepEqual(lhsV.Interface(), rhsV.Interface()) } +// isHashable returns true if the value can be used as a map key without +// panicking. A nil interface is a valid map key even though +// reflect.Value.Comparable reports false for it. +func isHashable(v reflect.Value) bool { + if v.Kind() == reflect.Interface && v.IsNil() { + return true + } + return v.Comparable() +} + +// hashableTypeString returns the type string of the value for error messages, +// unwrapping interfaces to show the dynamic type. +func hashableTypeString(v reflect.Value) string { + if v.Kind() == reflect.Interface && !v.IsNil() { + v = v.Elem() + } + return v.Type().String() +} + func getMapIndex(key reflect.Value, aMap reflect.Value) reflect.Value { if aMap.IsNil() { return nilValue @@ -264,6 +283,10 @@ func getMapIndex(key reflect.Value, aMap reflect.Value) reflect.Value { if err != nil { return nilValue } + if !isHashable(key) { + // an unhashable key can never be in a map + return nilValue + } // From reflect MapIndex: // It returns the zero Value if key is not found in the map or if v represents a nil map. diff --git a/vm/vmContainers_test.go b/vm/vmContainers_test.go index 23d40814..6fd41410 100644 --- a/vm/vmContainers_test.go +++ b/vm/vmContainers_test.go @@ -874,6 +874,14 @@ func TestMaps(t *testing.T) { {Script: `b[1] = 1`, RunError: fmt.Errorf("undefined symbol 'b'")}, {Script: `z.y.x = 1`, RunError: fmt.Errorf("undefined symbol 'z'")}, + // unhashable map keys should error instead of panic + {Script: `{[1,2]: 3}`, RunError: fmt.Errorf("type []interface {} cannot be used as map key")}, + {Script: `a = {}; a[[1,2]] = 3`, RunError: fmt.Errorf("type []interface {} cannot be used as map key"), Output: map[string]interface{}{"a": map[interface{}]interface{}{}}}, + {Script: `a = {}; a[{"x":1}] = 3`, RunError: fmt.Errorf("type map[interface {}]interface {} cannot be used as map key"), Output: map[string]interface{}{"a": map[interface{}]interface{}{}}}, + {Script: `a = {}; a[[1,2]]`, RunOutput: nil, Output: map[string]interface{}{"a": map[interface{}]interface{}{}}}, + {Script: `a = {"b": 1}; delete(a, [1,2])`, RunError: fmt.Errorf("type []interface {} cannot be used as map key in delete"), Output: map[string]interface{}{"a": map[interface{}]interface{}{"b": int64(1)}}}, + {Script: `a = {}; a[nil] = 1; a[nil]`, RunOutput: int64(1)}, + {Script: `{}`, RunOutput: map[interface{}]interface{}{}}, {Script: `{"b": nil}`, RunOutput: map[interface{}]interface{}{"b": nil}}, {Script: `{"b": true}`, RunOutput: map[interface{}]interface{}{"b": true}}, diff --git a/vm/vmExpr.go b/vm/vmExpr.go index a4f78102..ffb3630e 100644 --- a/vm/vmExpr.go +++ b/vm/vmExpr.go @@ -86,6 +86,11 @@ func (runInfo *runInfoStruct) invokeExpr() { return } key = runInfo.rv + if !isHashable(key) { + runInfo.err = newStringError(expr, "type "+hashableTypeString(key)+" cannot be used as map key") + runInfo.rv = nilValue + return + } runInfo.expr = expr.Values[i] runInfo.invokeExpr() @@ -132,6 +137,11 @@ func (runInfo *runInfoStruct) invokeExpr() { runInfo.rv = nilValue return } + if !isHashable(key) { + runInfo.err = newStringError(expr, "type "+hashableTypeString(key)+" cannot be used as map key") + runInfo.rv = nilValue + return + } runInfo.expr = expr.Values[i] runInfo.invokeExpr() diff --git a/vm/vmLetExpr.go b/vm/vmLetExpr.go index 0f2743ea..193a23e2 100644 --- a/vm/vmLetExpr.go +++ b/vm/vmLetExpr.go @@ -179,6 +179,11 @@ func (runInfo *runInfoStruct) invokeLetExpr() { runInfo.rv = nilValue return } + if !isHashable(runInfo.rv) { + runInfo.err = newStringError(expr, "type "+hashableTypeString(runInfo.rv)+" cannot be used as map key") + runInfo.rv = nilValue + return + } value, runInfo.err = convertReflectValueToType(value, item.Type().Elem()) if runInfo.err != nil { diff --git a/vm/vmStmt.go b/vm/vmStmt.go index 89e06f5a..df20d6e8 100644 --- a/vm/vmStmt.go +++ b/vm/vmStmt.go @@ -714,6 +714,11 @@ func (runInfo *runInfoStruct) runSingleStmt() { runInfo.rv = nilValue return } + if !isHashable(runInfo.rv) { + runInfo.err = newStringError(stmt, "type "+hashableTypeString(runInfo.rv)+" cannot be used as map key in delete") + runInfo.rv = nilValue + return + } item.SetMapIndex(runInfo.rv, reflect.Value{}) runInfo.rv = nilValue default: