Reduce the memory overhead in IP fragment management

- Deep-copy pkt.Data and hold it instead of shallow-copy (vv.Clone).
  This allows the pkt's backing array, which includes the header portion,
  to be freed.
- Remove fragHeap. The fragments are now held in holes struct instead.
- Stop reserving the initial capacity of holes slice.

PiperOrigin-RevId: 347198744
This commit is contained in:
Toshi Kikuchi
2020-12-12 15:32:03 -08:00
committed by gVisor bot
parent 4aef908c92
commit 08d36b6c63
6 changed files with 185 additions and 324 deletions
-2
View File
@@ -18,7 +18,6 @@ go_template_instance(
go_library(
name = "fragmentation",
srcs = [
"frag_heap.go",
"fragmentation.go",
"reassembler.go",
"reassembler_list.go",
@@ -38,7 +37,6 @@ go_test(
name = "fragmentation_test",
size = "small",
srcs = [
"frag_heap_test.go",
"fragmentation_test.go",
"reassembler_test.go",
],
@@ -1,77 +0,0 @@
// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fragmentation
import (
"container/heap"
"fmt"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
)
type fragment struct {
offset uint16
vv buffer.VectorisedView
}
type fragHeap []fragment
func (h *fragHeap) Len() int {
return len(*h)
}
func (h *fragHeap) Less(i, j int) bool {
return (*h)[i].offset < (*h)[j].offset
}
func (h *fragHeap) Swap(i, j int) {
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
}
func (h *fragHeap) Push(x interface{}) {
*h = append(*h, x.(fragment))
}
func (h *fragHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
// reassamble empties the heap and returns a VectorisedView
// containing a reassambled version of the fragments inside the heap.
func (h *fragHeap) reassemble() (buffer.VectorisedView, error) {
curr := heap.Pop(h).(fragment)
views := curr.vv.Views()
size := curr.vv.Size()
if curr.offset != 0 {
return buffer.VectorisedView{}, fmt.Errorf("offset of the first packet is != 0 (%d)", curr.offset)
}
for h.Len() > 0 {
curr := heap.Pop(h).(fragment)
if int(curr.offset) < size {
curr.vv.TrimFront(size - int(curr.offset))
} else if int(curr.offset) > size {
return buffer.VectorisedView{}, fmt.Errorf("packet has a hole, expected offset %d, got %d", size, curr.offset)
}
size += curr.vv.Size()
views = append(views, curr.vv.Views()...)
}
return buffer.NewVectorisedView(size, views), nil
}
@@ -1,126 +0,0 @@
// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fragmentation
import (
"container/heap"
"reflect"
"testing"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
)
var reassambleTestCases = []struct {
comment string
in []fragment
want buffer.VectorisedView
}{
{
comment: "Non-overlapping in-order",
in: []fragment{
{offset: 0, vv: vv(1, "0")},
{offset: 1, vv: vv(1, "1")},
},
want: vv(2, "0", "1"),
},
{
comment: "Non-overlapping out-of-order",
in: []fragment{
{offset: 1, vv: vv(1, "1")},
{offset: 0, vv: vv(1, "0")},
},
want: vv(2, "0", "1"),
},
{
comment: "Duplicated packets",
in: []fragment{
{offset: 0, vv: vv(1, "0")},
{offset: 0, vv: vv(1, "0")},
},
want: vv(1, "0"),
},
{
comment: "Overlapping in-order",
in: []fragment{
{offset: 0, vv: vv(2, "01")},
{offset: 1, vv: vv(2, "12")},
},
want: vv(3, "01", "2"),
},
{
comment: "Overlapping out-of-order",
in: []fragment{
{offset: 1, vv: vv(2, "12")},
{offset: 0, vv: vv(2, "01")},
},
want: vv(3, "01", "2"),
},
{
comment: "Overlapping subset in-order",
in: []fragment{
{offset: 0, vv: vv(3, "012")},
{offset: 1, vv: vv(1, "1")},
},
want: vv(3, "012"),
},
{
comment: "Overlapping subset out-of-order",
in: []fragment{
{offset: 1, vv: vv(1, "1")},
{offset: 0, vv: vv(3, "012")},
},
want: vv(3, "012"),
},
}
func TestReassamble(t *testing.T) {
for _, c := range reassambleTestCases {
t.Run(c.comment, func(t *testing.T) {
h := make(fragHeap, 0, 8)
heap.Init(&h)
for _, f := range c.in {
heap.Push(&h, f)
}
got, err := h.reassemble()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, c.want) {
t.Errorf("got reassemble(%+v) = %v, want = %v", c.in, got, c.want)
}
})
}
}
func TestReassambleFailsForNonZeroOffset(t *testing.T) {
h := make(fragHeap, 0, 8)
heap.Init(&h)
heap.Push(&h, fragment{offset: 1, vv: vv(1, "0")})
_, err := h.reassemble()
if err == nil {
t.Errorf("reassemble() did not fail when the first packet had offset != 0")
}
}
func TestReassambleFailsForHoles(t *testing.T) {
h := make(fragHeap, 0, 8)
heap.Init(&h)
heap.Push(&h, fragment{offset: 0, vv: vv(1, "0")})
heap.Push(&h, fragment{offset: 2, vv: vv(1, "1")})
_, err := h.reassemble()
if err == nil {
t.Errorf("reassemble() did not fail when there was a hole in the packet")
}
}
@@ -53,6 +53,10 @@ var (
// ErrFragmentOverlap indicates that, during reassembly, a fragment overlaps
// with another one.
ErrFragmentOverlap = errors.New("overlapping fragments")
// ErrFragmentConflict indicates that, during reassembly, some fragments are
// in conflict with one another.
ErrFragmentConflict = errors.New("conflicting fragments")
)
// FragmentID is the identifier for a fragment.
+87 -76
View File
@@ -15,9 +15,8 @@
package fragmentation
import (
"container/heap"
"fmt"
"math"
"sort"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/tcpip"
@@ -29,6 +28,8 @@ type hole struct {
first uint16
last uint16
filled bool
final bool
data buffer.View
}
type reassembler struct {
@@ -39,7 +40,6 @@ type reassembler struct {
mu sync.Mutex
holes []hole
filled int
heap fragHeap
done bool
creationTime int64
pkt *stack.PacketBuffer
@@ -48,74 +48,17 @@ type reassembler struct {
func newReassembler(id FragmentID, clock tcpip.Clock) *reassembler {
r := &reassembler{
id: id,
holes: make([]hole, 0, 16),
heap: make(fragHeap, 0, 8),
creationTime: clock.NowMonotonic(),
}
r.holes = append(r.holes, hole{
first: 0,
last: math.MaxUint16,
filled: false,
final: true,
})
return r
}
// updateHoles updates the list of holes for an incoming fragment. It returns
// true if the fragment fits, it is not a duplicate and it does not overlap with
// another fragment.
//
// For IPv6, overlaps with an existing fragment are explicitly forbidden by
// RFC 8200 section 4.5:
// If any of the fragments being reassembled overlap with any other fragments
// being reassembled for the same packet, reassembly of that packet must be
// abandoned and all the fragments that have been received for that packet
// must be discarded, and no ICMP error messages should be sent.
//
// It is not explicitly forbidden for IPv4, but to keep parity with Linux we
// disallow it as well:
// https://github.com/torvalds/linux/blob/38525c6/net/ipv4/inet_fragment.c#L349
func (r *reassembler) updateHoles(first, last uint16, more bool) (bool, error) {
for i := range r.holes {
currentHole := &r.holes[i]
if currentHole.filled || last < currentHole.first || currentHole.last < first {
continue
}
if first < currentHole.first || currentHole.last < last {
// Incoming fragment only partially fits in the free hole.
return false, ErrFragmentOverlap
}
r.filled++
if first > currentHole.first {
r.holes = append(r.holes, hole{
first: currentHole.first,
last: first - 1,
filled: false,
})
}
if last < currentHole.last && more {
r.holes = append(r.holes, hole{
first: last + 1,
last: currentHole.last,
filled: false,
})
}
// Update the current hole to precisely match the incoming fragment.
r.holes[i] = hole{
first: first,
last: last,
filled: true,
}
return true, nil
}
// Incoming fragment is a duplicate/subset, or its offset comes after the end
// of the reassembled payload.
return false, nil
}
func (r *reassembler) process(first, last uint16, more bool, proto uint8, pkt *stack.PacketBuffer) (buffer.VectorisedView, uint8, bool, int, error) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -126,13 +69,73 @@ func (r *reassembler) process(first, last uint16, more bool, proto uint8, pkt *s
return buffer.VectorisedView{}, 0, false, 0, nil
}
used, err := r.updateHoles(first, last, more)
if err != nil {
return buffer.VectorisedView{}, 0, false, 0, fmt.Errorf("fragment reassembly failed: %w", err)
}
var holeFound bool
var consumed int
if used {
for i := range r.holes {
currentHole := &r.holes[i]
if last < currentHole.first || currentHole.last < first {
continue
}
// For IPv6, overlaps with an existing fragment are explicitly forbidden by
// RFC 8200 section 4.5:
// If any of the fragments being reassembled overlap with any other
// fragments being reassembled for the same packet, reassembly of that
// packet must be abandoned and all the fragments that have been received
// for that packet must be discarded, and no ICMP error messages should be
// sent.
//
// It is not explicitly forbidden for IPv4, but to keep parity with Linux we
// disallow it as well:
// https://github.com/torvalds/linux/blob/38525c6/net/ipv4/inet_fragment.c#L349
if first < currentHole.first || currentHole.last < last {
// Incoming fragment only partially fits in the free hole.
return buffer.VectorisedView{}, 0, false, 0, ErrFragmentOverlap
}
if !more {
if !currentHole.final || currentHole.filled && currentHole.last != last {
// We have another final fragment, which does not perfectly overlap.
return buffer.VectorisedView{}, 0, false, 0, ErrFragmentConflict
}
}
holeFound = true
if currentHole.filled {
// Incoming fragment is a duplicate.
continue
}
// We are populating the current hole with the payload and creating a new
// hole for any unfilled ranges on either end.
if first > currentHole.first {
r.holes = append(r.holes, hole{
first: currentHole.first,
last: first - 1,
filled: false,
final: false,
})
}
if last < currentHole.last && more {
r.holes = append(r.holes, hole{
first: last + 1,
last: currentHole.last,
filled: false,
final: currentHole.final,
})
currentHole.final = false
}
v := pkt.Data.ToOwnedView()
consumed = v.Size()
r.size += consumed
// Update the current hole to precisely match the incoming fragment.
r.holes[i] = hole{
first: first,
last: last,
filled: true,
final: currentHole.final,
data: v,
}
r.filled++
// For IPv6, it is possible to have different Protocol values between
// fragments of a packet (because, unlike IPv4, the Protocol is not used to
// identify a fragment). In this case, only the Protocol of the first
@@ -145,22 +148,30 @@ func (r *reassembler) process(first, last uint16, more bool, proto uint8, pkt *s
r.pkt = pkt
r.proto = proto
}
vv := pkt.Data
// We store the incoming packet only if it filled some holes.
heap.Push(&r.heap, fragment{offset: first, vv: vv.Clone(nil)})
consumed = vv.Size()
r.size += consumed
break
}
if !holeFound {
// Incoming fragment is beyond end.
return buffer.VectorisedView{}, 0, false, 0, ErrFragmentConflict
}
// Check if all the holes have been filled and we are ready to reassemble.
if r.filled < len(r.holes) {
return buffer.VectorisedView{}, 0, false, consumed, nil
}
res, err := r.heap.reassemble()
if err != nil {
return buffer.VectorisedView{}, 0, false, 0, fmt.Errorf("fragment reassembly failed: %w", err)
sort.Slice(r.holes, func(i, j int) bool {
return r.holes[i].first < r.holes[j].first
})
var size int
views := make([]buffer.View, 0, len(r.holes))
for _, hole := range r.holes {
views = append(views, hole.data)
size += hole.data.Size()
}
return res, r.proto, true, consumed, nil
return buffer.NewVectorisedView(size, views), r.proto, true, consumed, nil
}
func (r *reassembler) checkDoneOrMark() bool {
@@ -19,105 +19,156 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
type updateHolesParams struct {
type processParams struct {
first uint16
last uint16
more bool
wantUsed bool
pkt *stack.PacketBuffer
wantDone bool
wantError error
}
func TestUpdateHoles(t *testing.T) {
func TestReassemblerProcess(t *testing.T) {
const proto = 99
v := func(size int) buffer.View {
payload := buffer.NewView(size)
for i := 1; i < size; i++ {
payload[i] = uint8(i) * 3
}
return payload
}
pkt := func(size int) *stack.PacketBuffer {
return stack.NewPacketBuffer(stack.PacketBufferOptions{
Data: v(size).ToVectorisedView(),
})
}
var tests = []struct {
name string
params []updateHolesParams
params []processParams
want []hole
}{
{
name: "No fragments",
params: nil,
want: []hole{{first: 0, last: math.MaxUint16, filled: false}},
want: []hole{{first: 0, last: math.MaxUint16, filled: false, final: true}},
},
{
name: "One fragment at beginning",
params: []updateHolesParams{{first: 0, last: 1, more: true, wantUsed: true, wantError: nil}},
params: []processParams{{first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil}},
want: []hole{
{first: 0, last: 1, filled: true},
{first: 2, last: math.MaxUint16, filled: false},
{first: 0, last: 1, filled: true, final: false, data: v(2)},
{first: 2, last: math.MaxUint16, filled: false, final: true},
},
},
{
name: "One fragment in the middle",
params: []updateHolesParams{{first: 1, last: 2, more: true, wantUsed: true, wantError: nil}},
params: []processParams{{first: 1, last: 2, more: true, pkt: pkt(2), wantDone: false, wantError: nil}},
want: []hole{
{first: 1, last: 2, filled: true},
{first: 0, last: 0, filled: false},
{first: 3, last: math.MaxUint16, filled: false},
{first: 1, last: 2, filled: true, final: false, data: v(2)},
{first: 0, last: 0, filled: false, final: false},
{first: 3, last: math.MaxUint16, filled: false, final: true},
},
},
{
name: "One fragment at the end",
params: []updateHolesParams{{first: 1, last: 2, more: false, wantUsed: true, wantError: nil}},
params: []processParams{{first: 1, last: 2, more: false, pkt: pkt(2), wantDone: false, wantError: nil}},
want: []hole{
{first: 1, last: 2, filled: true},
{first: 1, last: 2, filled: true, final: true, data: v(2)},
{first: 0, last: 0, filled: false},
},
},
{
name: "One fragment completing a packet",
params: []updateHolesParams{{first: 0, last: 1, more: false, wantUsed: true, wantError: nil}},
params: []processParams{{first: 0, last: 1, more: false, pkt: pkt(2), wantDone: true, wantError: nil}},
want: []hole{
{first: 0, last: 1, filled: true},
{first: 0, last: 1, filled: true, final: true, data: v(2)},
},
},
{
name: "Two fragments completing a packet",
params: []updateHolesParams{
{first: 0, last: 1, more: true, wantUsed: true, wantError: nil},
{first: 2, last: 3, more: false, wantUsed: true, wantError: nil},
params: []processParams{
{first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil},
{first: 2, last: 3, more: false, pkt: pkt(2), wantDone: true, wantError: nil},
},
want: []hole{
{first: 0, last: 1, filled: true},
{first: 2, last: 3, filled: true},
{first: 0, last: 1, filled: true, final: false, data: v(2)},
{first: 2, last: 3, filled: true, final: true, data: v(2)},
},
},
{
name: "Two fragments completing a packet with a duplicate",
params: []updateHolesParams{
{first: 0, last: 1, more: true, wantUsed: true, wantError: nil},
{first: 0, last: 1, more: true, wantUsed: false, wantError: nil},
{first: 2, last: 3, more: false, wantUsed: true, wantError: nil},
params: []processParams{
{first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil},
{first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil},
{first: 2, last: 3, more: false, pkt: pkt(2), wantDone: true, wantError: nil},
},
want: []hole{
{first: 0, last: 1, filled: true},
{first: 2, last: 3, filled: true},
{first: 0, last: 1, filled: true, final: false, data: v(2)},
{first: 2, last: 3, filled: true, final: true, data: v(2)},
},
},
{
name: "Two fragments completing a packet with a partial duplicate",
params: []processParams{
{first: 0, last: 3, more: true, pkt: pkt(4), wantDone: false, wantError: nil},
{first: 1, last: 2, more: true, pkt: pkt(2), wantDone: false, wantError: nil},
{first: 4, last: 5, more: false, pkt: pkt(2), wantDone: true, wantError: nil},
},
want: []hole{
{first: 0, last: 3, filled: true, final: false, data: v(4)},
{first: 4, last: 5, filled: true, final: true, data: v(2)},
},
},
{
name: "Two overlapping fragments",
params: []updateHolesParams{
{first: 0, last: 10, more: true, wantUsed: true, wantError: nil},
{first: 5, last: 15, more: false, wantUsed: false, wantError: ErrFragmentOverlap},
{first: 11, last: 15, more: false, wantUsed: true, wantError: nil},
params: []processParams{
{first: 0, last: 10, more: true, pkt: pkt(11), wantDone: false, wantError: nil},
{first: 5, last: 15, more: false, pkt: pkt(11), wantDone: false, wantError: ErrFragmentOverlap},
},
want: []hole{
{first: 0, last: 10, filled: true},
{first: 11, last: 15, filled: true},
{first: 0, last: 10, filled: true, final: false, data: v(11)},
{first: 11, last: math.MaxUint16, filled: false, final: true},
},
},
{
name: "Out of bounds fragment",
params: []updateHolesParams{
{first: 0, last: 10, more: true, wantUsed: true, wantError: nil},
{first: 11, last: 15, more: false, wantUsed: true, wantError: nil},
{first: 16, last: 20, more: false, wantUsed: false, wantError: nil},
name: "Two final fragments with different ends",
params: []processParams{
{first: 10, last: 14, more: false, pkt: pkt(5), wantDone: false, wantError: nil},
{first: 0, last: 9, more: false, pkt: pkt(10), wantDone: false, wantError: ErrFragmentConflict},
},
want: []hole{
{first: 0, last: 10, filled: true},
{first: 11, last: 15, filled: true},
{first: 10, last: 14, filled: true, final: true, data: v(5)},
{first: 0, last: 9, filled: false, final: false},
},
},
{
name: "Two final fragments - duplicate",
params: []processParams{
{first: 5, last: 14, more: false, pkt: pkt(10), wantDone: false, wantError: nil},
{first: 10, last: 14, more: false, pkt: pkt(5), wantDone: false, wantError: nil},
},
want: []hole{
{first: 5, last: 14, filled: true, final: true, data: v(10)},
{first: 0, last: 4, filled: false, final: false},
},
},
{
name: "Two final fragments - duplicate, with different ends",
params: []processParams{
{first: 5, last: 14, more: false, pkt: pkt(10), wantDone: false, wantError: nil},
{first: 10, last: 13, more: false, pkt: pkt(4), wantDone: false, wantError: ErrFragmentConflict},
},
want: []hole{
{first: 5, last: 14, filled: true, final: true, data: v(10)},
{first: 0, last: 4, filled: false, final: false},
},
},
}
@@ -126,9 +177,9 @@ func TestUpdateHoles(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
r := newReassembler(FragmentID{}, &faketime.NullClock{})
for _, param := range test.params {
used, err := r.updateHoles(param.first, param.last, param.more)
if used != param.wantUsed || err != param.wantError {
t.Errorf("got r.updateHoles(%d, %d, %t) = (%t, %v), want = (%t, %v)", param.first, param.last, param.more, used, err, param.wantUsed, param.wantError)
_, _, done, _, err := r.process(param.first, param.last, param.more, proto, param.pkt)
if done != param.wantDone || err != param.wantError {
t.Errorf("got r.process(%d, %d, %t, %d, _) = (_, _, %t, _, %v), want = (%t, %v)", param.first, param.last, param.more, proto, done, err, param.wantDone, param.wantError)
}
}
if diff := cmp.Diff(test.want, r.holes, cmp.AllowUnexported(hole{})); diff != "" {