fix: prevent overflow of int values
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
var Bool = basicnode.NewBool
|
||||
@@ -168,9 +169,17 @@ func anyAssemble(val any) qp.Assemble {
|
||||
case reflect.Bool:
|
||||
return qp.Bool(rv.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return qp.Int(rv.Int())
|
||||
i := rv.Int()
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
panic(fmt.Sprintf("integer %d exceeds safe bounds", i))
|
||||
}
|
||||
return qp.Int(i)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return qp.Int(int64(rv.Uint()))
|
||||
u := rv.Uint()
|
||||
if u > limits.MaxInt53 {
|
||||
panic(fmt.Sprintf("unsigned integer %d exceeds safe bounds", u))
|
||||
}
|
||||
return qp.Int(int64(u))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return qp.Float(rv.Float())
|
||||
case reflect.String:
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
@@ -214,7 +215,7 @@ func TestAny(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||
|
||||
v, err = Any(data["func"])
|
||||
_, err = Any(data["func"])
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -254,6 +255,59 @@ func BenchmarkAny(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnyAssembleIntegerOverflow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
input: 42,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "max safe int",
|
||||
input: limits.MaxInt53,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "min safe int",
|
||||
input: limits.MinInt53,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "overflow int",
|
||||
input: int64(limits.MaxInt53 + 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "underflow int",
|
||||
input: int64(limits.MinInt53 - 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "overflow uint",
|
||||
input: uint64(limits.MaxInt53 + 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.shouldErr {
|
||||
require.Panics(t, func() {
|
||||
anyAssemble(tt.input)
|
||||
})
|
||||
} else {
|
||||
require.NotPanics(t, func() {
|
||||
anyAssemble(tt.input)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -3,10 +3,13 @@ package policy
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
// Match determines if the IPLD node satisfies the policy.
|
||||
@@ -249,10 +252,26 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
|
||||
panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
|
||||
}
|
||||
|
||||
// isOrdered compares two IPLD nodes and returns true if they satisfy the given ordering function.
|
||||
// It supports comparison of integers and floats, returning false for:
|
||||
// - Nodes of different or unsupported kinds
|
||||
// - Integer values outside JavaScript's safe integer bounds (±2^53-1)
|
||||
// - Non-finite floating point values (NaN or ±Inf)
|
||||
//
|
||||
// The satisfies parameter is a function that interprets the comparison result:
|
||||
// - For ">" it returns true when order is 1
|
||||
// - For ">=" it returns true when order is 0 or 1
|
||||
// - For "<" it returns true when order is -1
|
||||
// - For "<=" it returns true when order is -1 or 0
|
||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
||||
a := must.Int(actual)
|
||||
b := must.Int(expected)
|
||||
|
||||
if a > limits.MaxInt53 || a < limits.MinInt53 || b > limits.MaxInt53 || b < limits.MinInt53 {
|
||||
return false
|
||||
}
|
||||
|
||||
return satisfies(cmp.Compare(a, b))
|
||||
}
|
||||
|
||||
@@ -265,6 +284,11 @@ func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) b
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("extracting selector float: %w", err))
|
||||
}
|
||||
|
||||
if math.IsInf(a, 0) || math.IsNaN(a) || math.IsInf(b, 0) || math.IsNaN(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
return satisfies(cmp.Compare(a, b))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
@@ -901,3 +902,38 @@ func TestPartialMatch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegerOverflow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expected ipld.Node
|
||||
actual ipld.Node
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "valid integers",
|
||||
expected: literal.Int(42),
|
||||
actual: literal.Int(43),
|
||||
want: true, // for gt comparison
|
||||
},
|
||||
{
|
||||
name: "exceeds MaxInt53",
|
||||
expected: literal.Int(limits.MaxInt53 + 1),
|
||||
actual: literal.Int(42),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "below MinInt53",
|
||||
expected: literal.Int(limits.MinInt53 - 1),
|
||||
actual: literal.Int(42),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isOrdered(tt.expected, tt.actual, gt)
|
||||
require.Equal(t, tt.want, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -56,6 +58,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid index", str, col, tok)
|
||||
}
|
||||
if idx > limits.MaxInt53 || idx < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("index %d exceeds safe integer bounds", idx), str, col, tok)
|
||||
}
|
||||
sel = append(sel, segment{str: tok, optional: opt, index: idx})
|
||||
|
||||
// explicit field, ["abcd"]
|
||||
@@ -77,6 +82,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid slice index", str, col, tok)
|
||||
}
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||
}
|
||||
rng[0] = i
|
||||
}
|
||||
if splt[1] == "" {
|
||||
@@ -86,6 +94,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid slice index", str, col, tok)
|
||||
}
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||
}
|
||||
rng[1] = i
|
||||
}
|
||||
sel = append(sel, segment{str: tok, optional: opt, slice: rng[:]})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package selector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
@@ -572,4 +574,23 @@ func TestParse(t *testing.T) {
|
||||
_, err := Parse(".[foo]")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("integer overflow", func(t *testing.T) {
|
||||
sel, err := Parse(fmt.Sprintf(".[%d]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
sel, err = Parse(fmt.Sprintf(".[%d]", limits.MinInt53-1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
// Test slice overflow
|
||||
sel, err = Parse(fmt.Sprintf(".[%d:42]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
sel, err = Parse(fmt.Sprintf(".[1:%d]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,19 +266,32 @@ func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
|
||||
case slice[0] == math.MinInt:
|
||||
start = 0
|
||||
case slice[0] < 0:
|
||||
// Check for potential overflow before adding
|
||||
if -slice[0] > length {
|
||||
start = 0
|
||||
} else {
|
||||
start = length + slice[0]
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case slice[1] == math.MaxInt:
|
||||
end = length
|
||||
case slice[1] < 0:
|
||||
// Check for potential overflow before adding
|
||||
if -slice[1] > length {
|
||||
end = 0
|
||||
} else {
|
||||
end = length + slice[1]
|
||||
}
|
||||
}
|
||||
|
||||
// backward iteration is not allowed, shortcut to an empty result
|
||||
if start >= end {
|
||||
start, end = 0, 0
|
||||
return
|
||||
}
|
||||
|
||||
// clamp out of bound
|
||||
if start < 0 {
|
||||
start = 0
|
||||
@@ -286,11 +299,14 @@ func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
|
||||
if start > length {
|
||||
start = length
|
||||
}
|
||||
if end < 0 {
|
||||
end = 0
|
||||
}
|
||||
if end > length {
|
||||
end = length
|
||||
}
|
||||
|
||||
return start, end
|
||||
return
|
||||
}
|
||||
|
||||
func kindString(n datamodel.Node) string {
|
||||
|
||||
@@ -2,6 +2,7 @@ package selector
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -356,3 +357,57 @@ func FuzzParseAndSelect(f *testing.F) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveSliceIndices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
slice []int64
|
||||
length int64
|
||||
wantStart int64
|
||||
wantEnd int64
|
||||
}{
|
||||
{
|
||||
name: "normal case",
|
||||
slice: []int64{1, 3},
|
||||
length: 5,
|
||||
wantStart: 1,
|
||||
wantEnd: 3,
|
||||
},
|
||||
{
|
||||
name: "negative indices",
|
||||
slice: []int64{-2, -1},
|
||||
length: 5,
|
||||
wantStart: 3,
|
||||
wantEnd: 4,
|
||||
},
|
||||
{
|
||||
name: "overflow protection negative start",
|
||||
slice: []int64{math.MinInt64, 3},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 3,
|
||||
},
|
||||
{
|
||||
name: "overflow protection negative end",
|
||||
slice: []int64{0, math.MinInt64},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 0,
|
||||
},
|
||||
{
|
||||
name: "max bounds",
|
||||
slice: []int64{0, math.MaxInt64},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start, end := resolveSliceIndices(tt.slice, tt.length)
|
||||
require.Equal(t, tt.wantStart, start)
|
||||
require.Equal(t, tt.wantEnd, end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user