google / google/gvisor

IFLA_NET_NS_PID inside VETH_INFO_PEER is ACKed and ignored

Open
#14,510 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

IFLA_NET_NS_PID in a veth peer block is ACKed and ignored, and the peer lands in the caller's own netns. At top level it returns EOPNOTSUPP. The block is only read for IFLA_IFNAME and IFLA_NET_NS_FD (stack.go:439).

Two creates pass the peer netns, by fd and by pid:

```
create veth_fd, peer IFLA_NET_NS_FD=5 -> ack ok
create veth_pid, peer IFLA_NET_NS_PID=12 -> ack ok

links in the caller's netns: [lo veth_fd veth_pid peer_by_pid]
links in the target netns: [lo peer_by_fd]
```

On Linux both peers arrive in the target.

Proposed fix in #14513.

reproducer

```go
// Run as root:
// go build -o netnspid . && sudo ./netnspid # Linux
// sudo runsc --network=none --ignore-cgroups do $PWD/netnspid # gVisor
package main

import (
"encoding/binary"
"fmt"
"os"
"os/exec"
"runtime"
"syscall"
"time"
)

const (
rtmNewLink = 16
rtmGetLink = 18

nlmFRequest = 0x001
nlmFAck = 0x004
nlmFExcl = 0x200
nlmFCreate = 0x400
nlmFDump = 0x300

nlmsgDone = 3
nlmsgError = 2

iflaIfname = 3
iflaLinkinfo = 18
iflaNetNsPid = 19
iflaNetNsFd = 28

iflaInfoKind = 1
iflaInfoData = 2
vethInfoPeer = 1

ifInfomsgSize = 16
nlmsghdrSize = 16

cloneNewnet = 0x40000000
sysSetns = 308
)

func main() {
stage := ""
if len(os.Args) > 1 {
stage = os.Args[1]
}
switch stage {
case "sleeper":
time.Sleep(time.Hour)
return
case "":
reexecInFreshNetns()
return
}

self, err := os.Executable()
if err != nil {
fail("executable", err)
}
sleeper := exec.Command(self, "sleeper")
sleeper.SysProcAttr = &syscall.SysProcAttr{Cloneflags: cloneNewnet}
err = sleeper.Start()
if err != nil {
fail("starting the process that holds the target netns", err)
}
defer sleeper.Process.Kill()

pid := sleeper.Process.Pid
targetNs, err := syscall.Open(fmt.Sprintf("/proc/%d/ns/net", pid), syscall.O_RDONLY, 0)
if err != nil {
fail("opening the target netns", err)
}
fmt.Printf("target netns: pid %d, fd %d (owned by our own user namespace)\n\n", pid, targetNs)

sock, err := openNetlink()
if err != nil {
fail("opening a netlink socket", err)
}

fdPeer := concat(attr(iflaIfname, nulString("peer_by_fd")), attr(iflaNetNsFd, u32(uint32(targetNs))))
fmt.Printf("create veth_fd, peer IFLA_NET_NS_FD=%d -> ack %v\n", targetNs, ack(newVeth(sock, "veth_fd", fdPeer)))

pidPeer := concat(attr(iflaIfname, nulString("peer_by_pid")), attr(iflaNetNsPid, u32(uint32(pid))))
fmt.Printf("create veth_pid, peer IFLA_NET_NS_PID=%d -> ack %v\n\n", pid, ack(newVeth(sock, "veth_pid", pidPeer)))

here, err := dumpLinks(sock)
if err != nil {
fail("dumping our own links", err)
}
there, err := linksIn(targetNs)
if err != nil {
fail("dumping the target's links", err)
}
fmt.Printf("links in the caller's netns: %v\n", here)
fmt.Printf("links in the target netns: %v\n\n", there)

if has(there, "peer_by_pid") {
fmt.Println("peer_by_pid reached the target netns")
return
}
fmt.Println("peer_by_pid was ACKed but never left the caller's netns")
}

func reexecInFreshNetns() {
self, err := os.Executable()
if err != nil {
fail("executable", err)
}
cmd := exec.Command(self, "probe")
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: cloneNewnet}
err = cmd.Run()
if err != nil {
fail("running the probe", err)
}
}

func newVeth(sock int, name string, peerAttrs []byte) error {
peer := attr(vethInfoPeer, concat(make([]byte, ifInfomsgSize), peerAttrs))
info := concat(attr(iflaInfoKind, nulString("veth")), attr(iflaInfoData, peer))
body := concat(make([]byte, ifInfomsgSize), attr(iflaIfname, nulString(name)), attr(iflaLinkinfo, info))
return request(sock, rtmNewLink, nlmFRequest|nlmFAck|nlmFCreate|nlmFExcl, body)
}

func linksIn(nsFd int) ([]string, error) {
type result struct {
links []string
err error
}
ch := make(chan result, 1)
go func() {
runtime.LockOSThread()
_, _, errno := syscall.Syscall(sysSetns, uintptr(nsFd), cloneNewnet, 0)
if errno != 0 {
ch <- result{err: fmt.Errorf("setns: %w", errno)}
return
}
sock, err := openNetlink()
if err != nil {
ch <- result{err: err}
return
}
defer syscall.Close(sock)
links, err := dumpLinks(sock)
ch <- result{links: links, err: err}
}()
r := <-ch
return r.links, r.err
}

func dumpLinks(sock int) ([]string, error) {
err := send(sock, rtmGetLink, nlmFRequest|nlmFDump, make([]byte, ifInfomsgSize))
if err != nil {
return nil, err
}

var links []string
for {
buf := make([]byte, 1<<16)
n, _, err := syscall.Recvfrom(sock, buf, 0)
if err != nil {
return nil, err
}
buf = buf[:n]
for len(buf) >= nlmsghdrSize {
msgLen := int(binary.NativeEndian.Uint32(buf))
if msgLen < nlmsghdrSize || msgLen > len(buf) {
return nil, fmt.Errorf("bad netlink message length %d", msgLen)
}
msgType := binary.NativeEndian.Uint16(buf[4:])
if msgType == nlmsgDone {
return links, nil
}
if msgType == nlmsgError {
return nil, errnoOf(buf)
}
name, ok := ifname(buf[nlmsghdrSize+ifInfomsgSize : msgLen])
if ok {
links = append(links, name)
}
buf = buf[align4(msgLen):]
}
}
}

func ifname(attrs []byte) (string, bool) {
for len(attrs) >= 4 {
attrLen := int(binary.NativeEndian.Uint16(attrs))
attrType := binary.NativeEndian.Uint16(attrs[2:])
if attrLen < 4 || attrLen > len(attrs) {
return "", false
}
if attrType == iflaIfname {
return string(trimNul(attrs[4:attrLen])), true
}
attrs = attrs[align4(attrLen):]
}
return "", false
}

func request(sock int, msgType, flags uint16, body []byte) error {
err := send(sock, msgType, flags, body)
if err != nil {
return err
}
buf := make([]byte, 1<<16)
n, _, err := syscall.Recvfrom(sock, buf, 0)
if err != nil {
return err
}
if n < nlmsghdrSize+4 {
return fmt.Errorf("netlink reply is %d bytes", n)
}
if binary.NativeEndian.Uint16(buf[4:]) != nlmsgError {
return fmt.Errorf("unexpected netlink reply type %d", binary.NativeEndian.Uint16(buf[4:]))
}
return errnoOf(buf[:n])
}

func send(sock int, msgType, flags uint16, body []byte) error {
msg := make([]byte, nlmsghdrSize+len(body))
binary.NativeEndian.PutUint32(msg, uint32(len(msg)))
binary.NativeEndian.PutUint16(msg[4:], msgType)
binary.NativeEndian.PutUint16(msg[6:], flags)
binary.NativeEndian.PutUint32(msg[8:], 1)
copy(msg[nlmsghdrSize:], body)
return syscall.Sendto(sock, msg, 0, &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK})
}

func openNetlink() (int, error) {
sock, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE)
if err != nil {
return -1, err
}
err = syscall.Bind(sock, &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK})
if err != nil {
syscall.Close(sock)
return -1, err
}
return sock, nil
}

func errnoOf(reply []byte) error {
code := int32(binary.NativeEndian.Uint32(reply[nlmsghdrSize:]))
if code == 0 {
return nil
}
return syscall.Errno(-code)
}

func ack(err error) string {
if err == nil {
return "ok"
}
return fmt.Sprintf("%v", err)
}

func attr(attrType uint16, payload []byte) []byte {
attrLen := 4 + len(payload)
b := make([]byte, align4(attrLen))
binary.NativeEndian.PutUint16(b, uint16(attrLen))
binary.NativeEndian.PutUint16(b[2:], attrType)
copy(b[4:], payload)
return b
}

func concat(parts ...[]byte) []byte {
var out []byte
for _, p := range parts {
out = append(out, p...)
}
return out
}

func nulString(s string) []byte {
return append([]byte(s), 0)
}

func trimNul(b []byte) []byte {
for i, c := range b {
if c == 0 {
return b[:i]
}
}
return b
}

func u32(v uint32) []byte {
b := make([]byte, 4)
binary.NativeEndian.PutUint32(b, v)
return b
}

func align4(n int) int {
return (n + 3) &^ 3
}

func has(links []string, name string) bool {
for _, l := range links {
if l == name {
return true
}
}
return false
}

func fail(what string, err error) {
fmt.Fprintf(os.Stderr, "%s: %v\n", what, err)
os.Exit(1)
}

```

Contributor guide

Open the contributing guide

Research direction

Start in stack.go around line 439, where the veth peer block is read only for IFLA_IFNAME and IFLA_NET_NS_FD. Run the provided Go reproducer under Linux and gVisor, compare the PID and FD cases, and use the Linux behavior as the expected result: the PID-selected peer should appear in the target netns while top-level IFLA_NET_NS_PID remains unsupported.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, linux
Domain
networking, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.