From edf5f259518dbd5b374da906b3ecd33de2b94a8d Mon Sep 17 00:00:00 2001 From: Johan Fylling Date: Fri, 10 Dec 2021 12:58:11 +0100 Subject: [PATCH] topdown: Fixing nanos int overflow issue for time.* built-in functions (#4117) * topdown: Fixing nanos int overflow issue for time.* built-in functions The go Time.UnixNano() function result is undefined for dates that cannot fit into an int64 when converted to Unix time in nanoseconds. Updating built-in functions to return error if date is too low/high. Fixes: #4098 Signed-off-by: Johan Fylling --- docs/content/policy-reference.md | 6 +- test/cases/testdata/time/test-time-0948.yaml | 131 ++++++------------ test/cases/testdata/time/test-time-0949.yaml | 135 ++++++------------- test/cases/testdata/time/test-time-0968.yaml | 98 ++------------ test/cases/testdata/time/test-time-0969.yaml | 97 ++----------- topdown/time.go | 49 ++++--- 6 files changed, 150 insertions(+), 366 deletions(-) diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md index ec0f19b08e..f134f00d1d 100644 --- a/docs/content/policy-reference.md +++ b/docs/content/policy-reference.md @@ -800,13 +800,13 @@ result_valid_hs256 := io.jwt.verify_hs256(result_hs256, "foo") | Built-in | Description | Wasm Support | | ------- |-------------|---------------| | ``output := time.now_ns()`` | ``output`` is a ``number`` representing the current time since epoch in nanoseconds. | ``SDK-dependent`` | -| ``output := time.parse_ns(layout, value)`` | ``output`` is a ``number`` representing the time ``value`` in nanoseconds since epoch. See the [Go `time` package documentation](https://golang.org/pkg/time/#Parse) for more details on ``layout``. | ``SDK-dependent`` | -| ``output := time.parse_rfc3339_ns(value)`` | ``output`` is a ``number`` representing the time ``value`` in nanoseconds since epoch. | ``SDK-dependent`` | +| ``output := time.parse_ns(layout, value)`` | ``output`` is a ``number`` representing the time ``value`` in nanoseconds since epoch; or ``undefined`` if outside the valid time range that can fit within an ``int64``. See the [Go `time` package documentation](https://golang.org/pkg/time/#Parse) for more details on ``layout``. | ``SDK-dependent`` | +| ``output := time.parse_rfc3339_ns(value)`` | ``output`` is a ``number`` representing the time ``value`` in nanoseconds since epoch; or ``undefined`` if outside the valid time range that can fit within an ``int64``. | ``SDK-dependent`` | | ``output := time.parse_duration_ns(duration)`` | ``output`` is a ``number`` representing the duration ``duration`` in nanoseconds. See the [Go `time` package documentation](https://golang.org/pkg/time/#ParseDuration) for more details on ``duration``. | ``SDK-dependent`` | | ``output := time.date(ns)``
``output := time.date([ns, tz])``
| ``output`` is of the form ``[year, month, day]``, which includes the ``year``, ``month`` (0-12), and ``day`` (0-31) as ``number``s representing the date from the nanoseconds since epoch (``ns``) in the timezone (``tz``), if supplied, or as UTC.| ``SDK-dependent`` | | ``output := time.clock(ns)``
``output := time.clock([ns, tz])``
| ``output`` is of the form ``[hour, minute, second]``, which outputs the ``hour``, ``minute`` (0-59), and ``second`` (0-59) as ``number``s representing the time of day for the nanoseconds since epoch (``ns``) in the timezone (``tz``), if supplied, or as UTC. | ``SDK-dependent`` | | ``day := time.weekday(ns)``
``day := time.weekday([ns, tz])``
| outputs the ``day`` as ``string`` representing the day of the week for the nanoseconds since epoch (``ns``) in the timezone (``tz``), if supplied, or as UTC. | ``SDK-dependent`` | -| ``output := time.add_date(ns, years, months, days)`` | ``output`` is a ``number`` representing the time since epoch in nanoseconds after adding the ``years``, ``months`` and ``days`` to ``ns``. See the [Go `time` package documentation](https://golang.org/pkg/time/#Time.AddDate) for more details on ``add_date``. | ``SDK-dependent`` | +| ``output := time.add_date(ns, years, months, days)`` | ``output`` is a ``number`` representing the time since epoch in nanoseconds after adding the ``years``, ``months`` and ``days`` to ``ns``; or ``undefined`` if outside the valid time range that can fit within an ``int64``. See the [Go `time` package documentation](https://golang.org/pkg/time/#Time.AddDate) for more details on ``add_date``. | ``SDK-dependent`` | | ``output := time.diff(ns1, ns2)``
``output := time.diff([ns1, tz1], [ns2, tz2])``
| ``output`` is of the form ``[year(s), month(s), day(s), hour(s), minute(s), second(s)]``, which outputs ``year(s)``, ``month(s)`` (0-11), ``day(s)`` (0-30), ``hour(s)``(0-23), ``minute(s)``(0-59) and ``second(s)``(0-59) as ``number``s representing the difference between the the two timestamps in nanoseconds since epoch (``ns1`` and ``ns2``), in the timezones (``tz1`` and ``tz2``, respectively), if supplied, or as UTC. | ``SDK-dependent`` | > Multiple calls to the `time.now_ns` built-in function within a single policy diff --git a/test/cases/testdata/time/test-time-0948.yaml b/test/cases/testdata/time/test-time-0948.yaml index a3593ff0a1..5eadffb222 100644 --- a/test/cases/testdata/time/test-time-0948.yaml +++ b/test/cases/testdata/time/test-time-0948.yaml @@ -1,95 +1,50 @@ cases: -- data: - a: - - 1 - - 2 - - 3 - - 4 - b: - v1: hello - v2: goodbye - c: - - x: - - true - - false - - foo - "y": - - null - - 3.14159 - z: - p: true - q: false - d: - e: - - bar - - baz - f: - - xs: - - 1 - ys: - - 2 - - xs: - - 2 - ys: - - 3 - g: - a: - - 1 - - 0 - - 0 - - 0 - b: - - 0 - - 2 - - 0 - - 0 - c: - - 0 - - 0 - - 0 - - 4 - h: - - - 1 - - 2 - - 3 - - - 2 - - 3 - - 4 - l: - - a: bob - b: -1 - c: - - 1 - - 2 - - 3 - - 4 - - a: alice - b: 1 - c: - - 2 - - 3 - - 4 - - 5 - d: null - m: [] - numbers: - - "1" - - "2" - - "3" - - "4" - strings: - bar: 2 - baz: 3 - foo: 1 - three: 3 +- note: time/parse_nanos modules: - | package generated - - p = ns { - time.parse_ns("2006-01-02T15:04:05Z07:00", "2017-06-02T19:00:00-07:00", ns) + + p[case_id] = ns { + case := input.cases[case_id] + time.parse_ns(case.layout, case.value, ns) } - note: time/parse nanos query: data.generated.p = x + input: { cases: { + 1: { layout: "2006-01-02T15:04:05Z07:00", value: "2017-06-02T19:00:00-07:00" }, # RFC3339 + 2: { layout: "2006-01-02T15:04:05Z07:00", value: "1677-09-21T00:12:43.145224192-00:00" }, # Earliest valid time + 3: { layout: "2006-01-02T15:04:05Z07:00", value: "2262-04-11T23:47:16.854775807-00:00" }, # Latest valid time + 4: { layout: "01/02 03:04:05PM '06 -0700", value: "06/02 07:00:00PM '17 -0700" }, # Layout + 5: { layout: "02 Jan 06 15:04 -0700", value: "02 Jun 17 19:00 -0700" }, # RFC822Z + }} want_result: - - x: 1496455200000000000 + - x: { + 1: 1496455200000000000, + 2: -9223372036854775808, + 3: 9223372036854775807, + 4: 1496455200000000000, + 5: 1496455200000000000 + } +- note: time/parse_nanos_too_small + modules: + - | + package generated + + p = ns { + time.parse_ns("2006-01-02T15:04:05Z07:00", "1677-09-21T00:12:43.145224191-00:00", ns) + } + query: data.generated.p = x + strict_error: true + want_error_code: eval_builtin_error + want_error: 'time outside of valid range' +- note: time/parse_nanos_too_large + modules: + - | + package generated + + p = ns { + time.parse_ns("2006-01-02T15:04:05Z07:00", "2262-04-11T23:47:16.854775808-00:00", ns) + } + query: data.generated.p = x + strict_error: true + want_error_code: eval_builtin_error + want_error: 'time outside of valid range' diff --git a/test/cases/testdata/time/test-time-0949.yaml b/test/cases/testdata/time/test-time-0949.yaml index cf73b1f501..73ad63e322 100644 --- a/test/cases/testdata/time/test-time-0949.yaml +++ b/test/cases/testdata/time/test-time-0949.yaml @@ -1,95 +1,48 @@ cases: -- data: - a: - - 1 - - 2 - - 3 - - 4 - b: - v1: hello - v2: goodbye - c: - - x: - - true - - false - - foo - "y": - - null - - 3.14159 - z: - p: true - q: false - d: - e: - - bar - - baz - f: - - xs: - - 1 - ys: - - 2 - - xs: - - 2 - ys: - - 3 - g: - a: - - 1 - - 0 - - 0 - - 0 - b: - - 0 - - 2 - - 0 - - 0 - c: - - 0 - - 0 - - 0 - - 4 - h: - - - 1 - - 2 - - 3 - - - 2 - - 3 - - 4 - l: - - a: bob - b: -1 - c: - - 1 - - 2 - - 3 - - 4 - - a: alice - b: 1 - c: - - 2 - - 3 - - 4 - - 5 - d: null - m: [] - numbers: - - "1" - - "2" - - "3" - - "4" - strings: - bar: 2 - baz: 3 - foo: 1 - three: 3 +- note: time/parse_rfc3339_nanos modules: - - | - package generated - - p = ns { - time.parse_rfc3339_ns("2017-06-02T19:00:00-07:00", ns) - } - note: time/parse rfc3339 nanos + - | + package generated + + p[t] = ns { + t = input.cases[_] + time.parse_rfc3339_ns(t, ns) + } query: data.generated.p = x + input: { cases: [ + "1677-09-21T00:12:43.145224192-00:00", # Earliest valid time + "1970-01-01T00:00:00-00:00", + "2017-06-02T19:00:00-07:00", + "2262-04-11T23:47:16.854775807-00:00" # Latest valid time + ]} want_result: - - x: 1496455200000000000 + - x: { + "1677-09-21T00:12:43.145224192-00:00": -9223372036854775808, + "1970-01-01T00:00:00-00:00": 0, + "2017-06-02T19:00:00-07:00": 1496455200000000000, + "2262-04-11T23:47:16.854775807-00:00": 9223372036854775807, + } +- note: time/parse_rfc3339_nanos_too_small + modules: + - | + package generated + + p = ns { + time.parse_rfc3339_ns("1677-09-21T00:12:43.145224191-00:00", ns) + } + query: data.generated.p = x + strict_error: true + want_error_code: eval_builtin_error + want_error: 'time outside of valid range' +- note: time/parse_rfc3339_nanos_too_large + modules: + - | + package generated + + p = ns { + time.parse_rfc3339_ns("2262-04-11T23:47:16.854775808-00:00", ns) + } + query: data.generated.p = x + strict_error: true + want_error_code: eval_builtin_error + want_error: 'time outside of valid range' diff --git a/test/cases/testdata/time/test-time-0968.yaml b/test/cases/testdata/time/test-time-0968.yaml index 08bc623d71..f7196da07d 100644 --- a/test/cases/testdata/time/test-time-0968.yaml +++ b/test/cases/testdata/time/test-time-0968.yaml @@ -1,87 +1,5 @@ cases: -- data: - a: - - 1 - - 2 - - 3 - - 4 - b: - v1: hello - v2: goodbye - c: - - x: - - true - - false - - foo - "y": - - null - - 3.14159 - z: - p: true - q: false - d: - e: - - bar - - baz - f: - - xs: - - 1 - ys: - - 2 - - xs: - - 2 - ys: - - 3 - g: - a: - - 1 - - 0 - - 0 - - 0 - b: - - 0 - - 2 - - 0 - - 0 - c: - - 0 - - 0 - - 0 - - 4 - h: - - - 1 - - 2 - - 3 - - - 2 - - 3 - - 4 - l: - - a: bob - b: -1 - c: - - 1 - - 2 - - 3 - - 4 - - a: alice - b: 1 - c: - - 2 - - 3 - - 4 - - 5 - d: null - m: [] - numbers: - - "1" - - "2" - - "3" - - "4" - strings: - bar: 2 - baz: 3 - foo: 1 - three: 3 +- note: time/add_date year month day modules: - | package generated @@ -90,7 +8,19 @@ cases: time.add_date(1585852421593912000, 3, 9, 12, __local1__) __local0__ = __local1__ } - note: time/add_date year month day + query: data.generated.p = x want_result: - x: 1705257221593912000 +- note: time/add_date too large result + modules: + - | + package generated + + p = ns { + time.add_date(0, 2262, 1, 1, ns) + } + query: data.generated.p = x + strict_error: true + want_error_code: eval_builtin_error + want_error: 'time outside of valid range' \ No newline at end of file diff --git a/test/cases/testdata/time/test-time-0969.yaml b/test/cases/testdata/time/test-time-0969.yaml index b554093f65..09929f4ff9 100644 --- a/test/cases/testdata/time/test-time-0969.yaml +++ b/test/cases/testdata/time/test-time-0969.yaml @@ -1,87 +1,5 @@ cases: -- data: - a: - - 1 - - 2 - - 3 - - 4 - b: - v1: hello - v2: goodbye - c: - - x: - - true - - false - - foo - "y": - - null - - 3.14159 - z: - p: true - q: false - d: - e: - - bar - - baz - f: - - xs: - - 1 - ys: - - 2 - - xs: - - 2 - ys: - - 3 - g: - a: - - 1 - - 0 - - 0 - - 0 - b: - - 0 - - 2 - - 0 - - 0 - c: - - 0 - - 0 - - 0 - - 4 - h: - - - 1 - - 2 - - 3 - - - 2 - - 3 - - 4 - l: - - a: bob - b: -1 - c: - - 1 - - 2 - - 3 - - 4 - - a: alice - b: 1 - c: - - 2 - - 3 - - 4 - - 5 - d: null - m: [] - numbers: - - "1" - - "2" - - "3" - - "4" - strings: - bar: 2 - baz: 3 - foo: 1 - three: 3 +- note: time/add_date negative values modules: - | package generated @@ -90,7 +8,18 @@ cases: time.add_date(1585852421593912000, -1, -1, -1, __local1__) __local0__ = __local1__ } - note: time/add_date negative values query: data.generated.p = x want_result: - x: 1551465221593912000 +- note: time/add_date too small result + modules: + - | + package generated + + p = ns { + time.add_date(-9223372036854775808, 0, 0, -1, ns) + } + query: data.generated.p = x + strict_error: true + want_error_code: eval_builtin_error + want_error: 'time outside of valid range' diff --git a/topdown/time.go b/topdown/time.go index b1e94eb651..481326dade 100644 --- a/topdown/time.go +++ b/topdown/time.go @@ -7,6 +7,7 @@ package topdown import ( "encoding/json" "fmt" + "math" "math/big" "strconv" "sync" @@ -19,43 +20,58 @@ import ( var tzCache map[string]*time.Location var tzCacheMutex *sync.Mutex +// 1677-09-21T00:12:43.145224192-00:00 +var minDateAllowedForNsConversion = time.Unix(0, math.MinInt64) + +// 2262-04-11T23:47:16.854775807-00:00 +var maxDateAllowedForNsConversion = time.Unix(0, math.MaxInt64) + +func toSafeUnixNano(t time.Time, iter func(*ast.Term) error) error { + if t.Before(minDateAllowedForNsConversion) || t.After(maxDateAllowedForNsConversion) { + return fmt.Errorf("time outside of valid range") + } + + return iter(ast.NewTerm(ast.Number(int64ToJSONNumber(t.UnixNano())))) +} + func builtinTimeNowNanos(bctx BuiltinContext, _ []*ast.Term, iter func(*ast.Term) error) error { return iter(bctx.Time) } -func builtinTimeParseNanos(a, b ast.Value) (ast.Value, error) { - +func builtinTimeParseNanos(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + a := operands[0].Value format, err := builtins.StringOperand(a, 1) if err != nil { - return nil, err + return err } + b := operands[1].Value value, err := builtins.StringOperand(b, 2) if err != nil { - return nil, err + return err } result, err := time.Parse(string(format), string(value)) if err != nil { - return nil, err + return err } - return ast.Number(int64ToJSONNumber(result.UnixNano())), nil + return toSafeUnixNano(result, iter) } -func builtinTimeParseRFC3339Nanos(a ast.Value) (ast.Value, error) { - +func builtinTimeParseRFC3339Nanos(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + a := operands[0].Value value, err := builtins.StringOperand(a, 1) if err != nil { - return nil, err + return err } result, err := time.Parse(time.RFC3339, string(value)) if err != nil { - return nil, err + return err } - return ast.Number(int64ToJSONNumber(result.UnixNano())), nil + return toSafeUnixNano(result, iter) } func builtinParseDurationNanos(a ast.Value) (ast.Value, error) { @@ -99,7 +115,7 @@ func builtinWeekday(a ast.Value) (ast.Value, error) { return ast.String(weekday), nil } -func builtinAddDate(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { +func builtinAddDate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { t, err := tzTime(operands[0].Value) if err != nil { return err @@ -121,10 +137,11 @@ func builtinAddDate(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Te } result := t.AddDate(years, months, days) - return iter(ast.NewTerm(ast.Number(int64ToJSONNumber(result.UnixNano())))) + + return toSafeUnixNano(result, iter) } -func builtinDiff(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { +func builtinDiff(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { t1, err := tzTime(operands[0].Value) if err != nil { return err @@ -267,8 +284,8 @@ func int64ToJSONNumber(i int64) json.Number { func init() { RegisterBuiltinFunc(ast.NowNanos.Name, builtinTimeNowNanos) - RegisterFunctionalBuiltin1(ast.ParseRFC3339Nanos.Name, builtinTimeParseRFC3339Nanos) - RegisterFunctionalBuiltin2(ast.ParseNanos.Name, builtinTimeParseNanos) + RegisterBuiltinFunc(ast.ParseRFC3339Nanos.Name, builtinTimeParseRFC3339Nanos) + RegisterBuiltinFunc(ast.ParseNanos.Name, builtinTimeParseNanos) RegisterFunctionalBuiltin1(ast.ParseDurationNanos.Name, builtinParseDurationNanos) RegisterFunctionalBuiltin1(ast.Date.Name, builtinDate) RegisterFunctionalBuiltin1(ast.Clock.Name, builtinClock)