google / google/gvisor

commonPrefixLen panics the Sentry on a short route destination

Open
#14,521 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
19.3k
Forks
2k
Avg merge
3d 5h
Merged PRs (30d)
264

Description

commonPrefixLen bounds its first argument and indexes its second, so a route destination shorter than the one asked for panics the Sentry:

```go
func commonPrefixLen(a, b []byte) (cpl int) {
for len(a) > 0 {
if a[0] == b[0] {
```

Netstack never checks the RTA_DST length, so a 17-byte one whose first 16 match the ::1/128 route every sandbox carries runs one step too far. Under hostinet a host default route with no gateway arrives with no destination at all, and ip route get 127.0.0.1 reaches it. Neither needs a capability.

Both kill runsc with exit 2:

```
panic: runtime error: index out of range [0] with length 0
```

Proposed fix in #14522.

reproducer

```go
// Run as root:
// go build -o longdst . && sudo ./longdst # Linux
// sudo runsc --network=none --ignore-cgroups do $PWD/longdst # gVisor
//
// The argument is how many bytes to add past a 16-byte RTA_DST, default 1.
package main

import (
"encoding/binary"
"fmt"
"os"
"strconv"
"syscall"
)

const (
rtmNewRoute = 24
rtmGetRoute = 26
rtaDst = 1
nlmsgError = 2
nlmsgDone = 3
nlmsgHdrLen = 16
rtMsgLen = 12
rtAttrLen = 4
ipv4PrefixLen = 32
ipv6PrefixLen = 128
dumpFlags = syscall.NLM_F_REQUEST | syscall.NLM_F_DUMP
)

type netlinkSocket struct {
fd int
seq uint32
}

func main() {
extra := 1
if len(os.Args) > 1 {
parsed, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "parsing extra byte count: %v\n", err)
os.Exit(1)
}
extra = parsed
}

sock, err := newNetlinkSocket()
if err != nil {
fmt.Fprintf(os.Stderr, "opening netlink socket: %v\n", err)
os.Exit(1)
}
defer sock.close()

loopback := make([]byte, 16)
loopback[15] = 1

sock.dumpTable()
sock.probe("well formed 127.0.0.1", []byte{127, 0, 0, 1})
sock.probe("well formed ::1", loopback)
if extra > 0 {
unmatched := make([]byte, 16+extra)
for i := range unmatched {
unmatched[i] = 0xff
}
sock.probe("oversized, no route shares its prefix", unmatched)

matched := make([]byte, 16+extra)
copy(matched, loopback)
sock.probe("oversized, prefix matches the ::1 route", matched)
}
sock.probe("well formed ::1 again", loopback)
fmt.Println("no panic")
}

func (s *netlinkSocket) dumpTable() {
fmt.Println("== route dump ==")
err := s.send(dumpFlags, 0 /* family */, 0 /* dstLen */, nil /* dst */)
if err != nil {
fmt.Printf(" send: %v\n", err)
return
}

for {
reply, err := s.recv()
if err != nil {
fmt.Printf(" recv: %v\n", err)
return
}

if s.printReply(reply) {
return
}
}
}

func (s *netlinkSocket) probe(label string, dst []byte) {
fmt.Printf("== %s: RTA_DST of %d bytes ==\n", label, len(dst))
family, prefixLen := uint8(syscall.AF_INET6), uint8(ipv6PrefixLen)
if len(dst) == 4 {
family, prefixLen = syscall.AF_INET, ipv4PrefixLen
}

err := s.send(syscall.NLM_F_REQUEST, family, prefixLen, dst)
if err != nil {
fmt.Printf(" send: %v\n", err)
return
}

reply, err := s.recv()
if err != nil {
fmt.Printf(" recv: %v\n", err)
return
}
s.printReply(reply)
}

func newNetlinkSocket() (*netlinkSocket, error) {
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE)
if err != nil {
return nil, err
}

err = syscall.Bind(fd, &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK})
if err != nil {
syscall.Close(fd)
return nil, err
}

timeout := syscall.Timeval{Sec: 5}
err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout)
if err != nil {
syscall.Close(fd)
return nil, err
}
return &netlinkSocket{fd: fd}, nil
}

func (s *netlinkSocket) close() {
if s == nil {
return
}

syscall.Close(s.fd)
}

func (s *netlinkSocket) send(flags uint16, family, dstLen uint8, dst []byte) error {
s.seq++
msg := make([]byte, nlmsgHdrLen+rtMsgLen)
binary.LittleEndian.PutUint16(msg[4:6], rtmGetRoute)
binary.LittleEndian.PutUint16(msg[6:8], flags)
binary.LittleEndian.PutUint32(msg[8:12], s.seq)
msg[nlmsgHdrLen] = family
msg[nlmsgHdrLen+1] = dstLen

if len(dst) > 0 {
attrLen := rtAttrLen + len(dst)
attr := make([]byte, rtAttrLen)
binary.LittleEndian.PutUint16(attr[0:2], uint16(attrLen))
binary.LittleEndian.PutUint16(attr[2:4], rtaDst)
msg = append(msg, attr...)
msg = append(msg, dst...)
msg = append(msg, make([]byte, (4-attrLen%4)%4)...)
}

binary.LittleEndian.PutUint32(msg[0:4], uint32(len(msg)))
return syscall.Sendto(s.fd, msg, 0 /* flags */, &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK})
}

func (s *netlinkSocket) recv() ([]byte, error) {
buf := make([]byte, 65536)
n, _, err := syscall.Recvfrom(s.fd, buf, 0 /* flags */)
if err != nil {
return nil, err
}
return buf[:n], nil
}

func (s *netlinkSocket) printReply(reply []byte) bool {
for len(reply) >= nlmsgHdrLen {
msgLen := int(binary.LittleEndian.Uint32(reply[0:4]))
msgType := binary.LittleEndian.Uint16(reply[4:6])
if msgLen < nlmsgHdrLen || msgLen > len(reply) {
fmt.Println(" truncated reply")
return true
}

switch msgType {
case nlmsgDone:
fmt.Println(" done")
return true

case nlmsgError:
errno := int32(binary.LittleEndian.Uint32(reply[nlmsgHdrLen : nlmsgHdrLen+4]))
if errno == 0 {
fmt.Println(" ack")
} else {
fmt.Printf(" error %v\n", syscall.Errno(-errno))
}

case rtmNewRoute:
s.printRoute(reply[nlmsgHdrLen:msgLen])

default:
fmt.Printf(" message type %d\n", msgType)
}

next := (msgLen + 3) &^ 3
if next >= len(reply) {
return false
}
reply = reply[next:]
}
return false
}

func (s *netlinkSocket) printRoute(body []byte) {
if len(body) < rtMsgLen {
return
}

dst := "none"
attrs := body[rtMsgLen:]
for len(attrs) >= rtAttrLen {
attrLen := int(binary.LittleEndian.Uint16(attrs[0:2]))
attrType := binary.LittleEndian.Uint16(attrs[2:4])
if attrLen < rtAttrLen || attrLen > len(attrs) {
break
}

if attrType == rtaDst {
dst = fmt.Sprintf("%x", attrs[rtAttrLen:attrLen])
}

next := (attrLen + 3) &^ 3
if next >= len(attrs) {
break
}
attrs = attrs[next:]
}
fmt.Printf(" route family=%d dstlen=%d dst=%s\n", body[0], body[1], dst)
}
```

Contributor guide

Open the contributing guide

Research direction

Search the Go netstack and hostinet route-handling code for commonPrefixLen, since no file or test is named. Build and run the supplied netlink reproducer in the issue, including oversized and empty destinations; done means both cases complete without a runsc panic or exit 2.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, linux
Domain
networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.