From 61bf784092a751ed0d5404310a74ac8042af3dba Mon Sep 17 00:00:00 2001 From: jiceatscion <139873336+jiceatscion@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:43:51 +0100 Subject: [PATCH] paths: Add check for hopfield count <= 64 when deserializing a scion path (#4483) Added check and added unit tests for deserialization and for router ingress. Fixes #4482 --- pkg/slayers/path/scion/BUILD.bazel | 1 + pkg/slayers/path/scion/base.go | 25 ++++++++++++--- pkg/slayers/path/scion/decoded.go | 7 ----- pkg/slayers/path/scion/raw_test.go | 25 ++++++++++++++- router/dataplane_test.go | 50 ++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 12 deletions(-) diff --git a/pkg/slayers/path/scion/BUILD.bazel b/pkg/slayers/path/scion/BUILD.bazel index a4a57739fa..c05261537c 100644 --- a/pkg/slayers/path/scion/BUILD.bazel +++ b/pkg/slayers/path/scion/BUILD.bazel @@ -24,6 +24,7 @@ go_test( ], deps = [ ":go_default_library", + "//pkg/private/serrors:go_default_library", "//pkg/slayers/path:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", diff --git a/pkg/slayers/path/scion/base.go b/pkg/slayers/path/scion/base.go index b2a7812239..6853fd49c6 100644 --- a/pkg/slayers/path/scion/base.go +++ b/pkg/slayers/path/scion/base.go @@ -22,10 +22,17 @@ import ( "github.com/scionproto/scion/pkg/slayers/path" ) -// MetaLen is the length of the PathMetaHeader. -const MetaLen = 4 +const ( + // MaxINFs is the maximum number of info fields in a SCION path. + MaxINFs = 3 + // MaxHops is the maximum number of hop fields in a SCION path. + MaxHops = 64 -const PathType path.Type = 1 + // MetaLen is the length of the PathMetaHeader. + MetaLen = 4 + + PathType path.Type = 1 +) func RegisterPath() { path.RegisterPath(path.Metadata{ @@ -66,6 +73,14 @@ func (s *Base) DecodeFromBytes(data []byte) error { } s.NumHops += int(s.PathMeta.SegLen[i]) } + + // We must check the validity of NumHops. It is possible to fit more than 64 hops in + // the length of a scion header. Yet a path of more than 64 hops cannot be followed to + // the end because CurrHF is only 6 bits long. + if s.NumHops > MaxHops { + return serrors.New("NumHops too large", "NumHops", s.NumHops, "Maximum", MaxHops) + } + return nil } @@ -113,7 +128,9 @@ func (s *Base) infIndexForHF(hf uint8) uint8 { } } -// Len returns the length of the path in bytes. +// Len returns the length of the path in bytes. That is, the number of byte required to +// store it, based on the metadata. The actual number of bytes available to contain it +// can be inferred from the common header field HdrLen. It may or may not be consistent. func (s *Base) Len() int { return MetaLen + s.NumINF*path.InfoLen + s.NumHops*path.HopLen } diff --git a/pkg/slayers/path/scion/decoded.go b/pkg/slayers/path/scion/decoded.go index da183f2903..c3ce572aa7 100644 --- a/pkg/slayers/path/scion/decoded.go +++ b/pkg/slayers/path/scion/decoded.go @@ -19,13 +19,6 @@ import ( "github.com/scionproto/scion/pkg/slayers/path" ) -const ( - // MaxINFs is the maximum number of info fields in a SCION path. - MaxINFs = 3 - // MaxHops is the maximum number of hop fields in a SCION path. - MaxHops = 64 -) - // Decoded implements the SCION (data-plane) path type. Decoded is intended to be used in // non-performance critical code paths, where the convenience of having a fully parsed path trumps // the loss of performance. diff --git a/pkg/slayers/path/scion/raw_test.go b/pkg/slayers/path/scion/raw_test.go index ff527d9398..3a869b0164 100644 --- a/pkg/slayers/path/scion/raw_test.go +++ b/pkg/slayers/path/scion/raw_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/slayers/path" "github.com/scionproto/scion/pkg/slayers/path/scion" ) @@ -51,6 +52,19 @@ var emptyRawTestPath = &scion.Raw{ Raw: make([]byte, scion.MetaLen), } +var overlongPath = &scion.Raw{ + Base: scion.Base{ + PathMeta: scion.MetaHdr{ + CurrINF: 0, + CurrHF: 0, + SegLen: [3]uint8{24, 24, 17}, + }, + NumINF: 3, + NumHops: 65, + }, + Raw: rawPath, +} + func TestRawSerialize(t *testing.T) { b := make([]byte, rawTestPath.Len()) assert.NoError(t, rawTestPath.SerializeTo(b)) @@ -63,7 +77,7 @@ func TestRawDecodeFromBytes(t *testing.T) { assert.Equal(t, rawTestPath, s) } -func TestRawSerliazeDecode(t *testing.T) { +func TestRawSerializeDecode(t *testing.T) { b := make([]byte, rawTestPath.Len()) assert.NoError(t, rawTestPath.SerializeTo(b)) s := &scion.Raw{} @@ -71,6 +85,15 @@ func TestRawSerliazeDecode(t *testing.T) { assert.Equal(t, rawTestPath, s) } +func TestOverlongSerializeDecode(t *testing.T) { + b := make([]byte, overlongPath.Len()) + assert.NoError(t, overlongPath.SerializeTo(b)) // permitted, if only to enable this test. + s := &scion.Raw{} + expected := serrors.New("NumHops too large", "NumHops", 65, "Maximum", scion.MaxHops) + err := s.DecodeFromBytes(b) + assert.Equal(t, expected.Error(), err.Error()) +} + func TestRawReverse(t *testing.T) { for name, tc := range pathReverseCases { name, tc := name, tc diff --git a/router/dataplane_test.go b/router/dataplane_test.go index baf163fb53..d0879885ba 100644 --- a/router/dataplane_test.go +++ b/router/dataplane_test.go @@ -34,6 +34,7 @@ import ( "github.com/scionproto/scion/pkg/addr" libepic "github.com/scionproto/scion/pkg/experimental/epic" + "github.com/scionproto/scion/pkg/private/serrors" "github.com/scionproto/scion/pkg/private/util" "github.com/scionproto/scion/pkg/private/xtest" "github.com/scionproto/scion/pkg/scrypto" @@ -620,6 +621,55 @@ func TestProcessPkt(t *testing.T) { egressInterface: 0, assertFunc: assert.NoError, }, + "inbound_longpath": { + prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { + return router.NewDP(fakeExternalInterfaces, + nil, mock_router.NewMockBatchConn(ctrl), + fakeInternalNextHops, nil, + xtest.MustParseIA("1-ff00:0:110"), nil, key) + }, + mockMsg: func(afterProcessing bool) *ipv4.Message { + spkt, dpath := prepBaseMsg(now) + spkt.DstIA = xtest.MustParseIA("1-ff00:0:110") + dst := addr.MustParseHost("10.0.100.100") + _ = spkt.SetDstAddr(dst) + dpath.HopFields = []path.HopField{ + {ConsIngress: 41, ConsEgress: 40}, + {ConsIngress: 31, ConsEgress: 30}, + {ConsIngress: 1, ConsEgress: 0}, + } + + // Everything is the same a in the inbound test, except that we tossed in + // 64 extra hops and two extra segments. + dpath.Base.PathMeta.CurrHF = 2 + dpath.Base.PathMeta.SegLen = [3]uint8{24, 24, 17} + dpath.InfoFields = append( + dpath.InfoFields, + path.InfoField{SegID: 0x112, ConsDir: true, Timestamp: util.TimeToSecs(now)}, + path.InfoField{SegID: 0x113, ConsDir: true, Timestamp: util.TimeToSecs(now)}, + ) + dpath.Base.NumINF = 3 + dpath.Base.NumHops = 65 + + dpath.HopFields[2].Mac = computeMAC(t, key, dpath.InfoFields[0], dpath.HopFields[2]) + ret := toMsg(t, spkt, dpath) + if afterProcessing { + ret.Addr = &net.UDPAddr{IP: dst.IP().AsSlice(), Port: topology.EndhostPort} + ret.Flags, ret.NN, ret.N, ret.OOB = 0, 0, 0, nil + } + return ret + }, + srcInterface: 1, + egressInterface: 0, + assertFunc: func(t assert.TestingT, err error, _ ...interface{}) bool { + if !assert.Error(t, err) { + return false + } + expected := serrors.New("NumHops too large", + "NumHops", 65, "Maximum", scion.MaxHops) + return assert.Equal(t, expected.Error(), err.Error()) + }, + }, "outbound": { prepareDP: func(ctrl *gomock.Controller) *router.DataPlane { return router.NewDP(