...

Source file src/github.com/opencontainers/runc/libcontainer/seccomp/patchbpf/enosys_linux_test.go

Documentation: github.com/opencontainers/runc/libcontainer/seccomp/patchbpf

     1  //go:build cgo && seccomp
     2  // +build cgo,seccomp
     3  
     4  package patchbpf
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/binary"
     9  	"fmt"
    10  	"testing"
    11  
    12  	"github.com/opencontainers/runc/libcontainer/configs"
    13  
    14  	libseccomp "github.com/seccomp/libseccomp-golang"
    15  	"golang.org/x/net/bpf"
    16  )
    17  
    18  type seccompData struct {
    19  	Syscall uint32 // NOTE: We assume sizeof(int) == 4.
    20  	Arch    uint32
    21  	IP      uint64
    22  	Args    [6]uint64
    23  }
    24  
    25  // mockSyscallPayload creates a fake seccomp_data struct with the given data.
    26  func mockSyscallPayload(t *testing.T, sysno libseccomp.ScmpSyscall, arch nativeArch, args ...uint64) []byte {
    27  	var buf bytes.Buffer
    28  
    29  	data := seccompData{
    30  		Syscall: uint32(sysno),
    31  		Arch:    uint32(arch),
    32  		IP:      0xDEADBEEFCAFE,
    33  	}
    34  
    35  	copy(data.Args[:], args)
    36  	if len(args) > 6 {
    37  		t.Fatalf("bad syscall payload: linux only supports 6-argument syscalls")
    38  	}
    39  
    40  	// NOTE: We use BigEndian here because golang.org/x/net/bpf assumes that
    41  	//       all payloads are big-endian while seccomp uses host endianness.
    42  	if err := binary.Write(&buf, binary.BigEndian, data); err != nil {
    43  		t.Fatalf("bad syscall payload: cannot write data: %v", err)
    44  	}
    45  	return buf.Bytes()
    46  }
    47  
    48  // retFallthrough is returned by the mockFilter. If a the mock filter returns
    49  // this value, it indicates "fallthrough to libseccomp-generated filter".
    50  const retFallthrough uint32 = 0xDEADBEEF
    51  
    52  // mockFilter returns a BPF VM that contains a mock filter with an -ENOSYS
    53  // stub. If the filter returns retFallthrough, the stub filter has permitted
    54  // the syscall to pass.
    55  func mockFilter(t *testing.T, config *configs.Seccomp) (*bpf.VM, []bpf.Instruction) {
    56  	patch, err := generatePatch(config)
    57  	if err != nil {
    58  		t.Fatalf("mock filter: generate enosys patch: %v", err)
    59  	}
    60  
    61  	program := append(patch, bpf.RetConstant{Val: retFallthrough})
    62  
    63  	vm, err := bpf.NewVM(program)
    64  	if err != nil {
    65  		t.Fatalf("mock filter: compile BPF VM: %v", err)
    66  	}
    67  	return vm, program
    68  }
    69  
    70  // fakeConfig generates a fake libcontainer seccomp configuration. The syscalls
    71  // are added with an action distinct from the default action.
    72  func fakeConfig(defaultAction configs.Action, explicitSyscalls []string, arches []string) *configs.Seccomp {
    73  	config := configs.Seccomp{
    74  		DefaultAction: defaultAction,
    75  		Architectures: arches,
    76  	}
    77  	syscallAction := configs.Allow
    78  	if syscallAction == defaultAction {
    79  		syscallAction = configs.Kill
    80  	}
    81  	for _, syscall := range explicitSyscalls {
    82  		config.Syscalls = append(config.Syscalls, &configs.Syscall{
    83  			Name:   syscall,
    84  			Action: syscallAction,
    85  		})
    86  	}
    87  	return &config
    88  }
    89  
    90  // List copied from <libcontainer/seccomp/config.go>.
    91  var testArches = []string{
    92  	"x86",
    93  	"amd64",
    94  	"x32",
    95  	"arm",
    96  	"arm64",
    97  	"mips",
    98  	"mips64",
    99  	"mips64n32",
   100  	"mipsel",
   101  	"mipsel64",
   102  	"mipsel64n32",
   103  	"ppc",
   104  	"ppc64",
   105  	"ppc64le",
   106  	"s390",
   107  	"s390x",
   108  }
   109  
   110  func testEnosysStub(t *testing.T, defaultAction configs.Action, arches []string) {
   111  	explicitSyscalls := []string{
   112  		"setns",
   113  		"kcmp",
   114  		"renameat2",
   115  		"copy_file_range",
   116  	}
   117  
   118  	implicitSyscalls := []string{
   119  		"clone",
   120  		"openat",
   121  		"read",
   122  		"write",
   123  	}
   124  
   125  	futureSyscalls := []libseccomp.ScmpSyscall{1000, 7331}
   126  
   127  	// Quick lookups for which arches are enabled.
   128  	archSet := map[string]bool{}
   129  	for _, arch := range arches {
   130  		archSet[arch] = true
   131  	}
   132  
   133  	for _, test := range []struct {
   134  		start, end int
   135  	}{
   136  		{0, 1}, // [setns]
   137  		{0, 2}, // [setns, process_vm_readv]
   138  		{1, 2}, // [process_vm_readv]
   139  		{1, 3}, // [process_vm_readv, renameat2, copy_file_range]
   140  		{1, 4}, // [process_vm_readv, renameat2, copy_file_range]
   141  		{3, 4}, // [copy_file_range]
   142  	} {
   143  		allowedSyscalls := explicitSyscalls[test.start:test.end]
   144  		config := fakeConfig(defaultAction, allowedSyscalls, arches)
   145  		filter, program := mockFilter(t, config)
   146  
   147  		// The syscalls are in increasing order of newness, so all syscalls
   148  		// after the last allowed syscall will give -ENOSYS.
   149  		enosysStart := test.end
   150  
   151  		for _, arch := range testArches {
   152  			type syscallTest struct {
   153  				syscall  string
   154  				sysno    libseccomp.ScmpSyscall
   155  				expected uint32
   156  			}
   157  
   158  			scmpArch, err := libseccomp.GetArchFromString(arch)
   159  			if err != nil {
   160  				t.Fatalf("unknown libseccomp architecture %q: %v", arch, err)
   161  			}
   162  
   163  			nativeArch, err := archToNative(scmpArch)
   164  			if err != nil {
   165  				t.Fatalf("unknown audit architecture %q: %v", arch, err)
   166  			}
   167  
   168  			var syscallTests []syscallTest
   169  
   170  			// Add explicit syscalls (whether they will return -ENOSYS
   171  			// depends on the filter rules).
   172  			for idx, syscall := range explicitSyscalls {
   173  				expected := retFallthrough
   174  				if idx >= enosysStart {
   175  					expected = retErrnoEnosys
   176  				}
   177  				sysno, err := libseccomp.GetSyscallFromNameByArch(syscall, scmpArch)
   178  				if err != nil {
   179  					t.Fatalf("unknown syscall %q on arch %q: %v", syscall, arch, err)
   180  				}
   181  				syscallTests = append(syscallTests, syscallTest{
   182  					syscall,
   183  					sysno,
   184  					expected,
   185  				})
   186  			}
   187  
   188  			// Add implicit syscalls.
   189  			for _, syscall := range implicitSyscalls {
   190  				sysno, err := libseccomp.GetSyscallFromNameByArch(syscall, scmpArch)
   191  				if err != nil {
   192  					t.Fatalf("unknown syscall %q on arch %q: %v", syscall, arch, err)
   193  				}
   194  				syscallTests = append(syscallTests, syscallTest{
   195  					sysno:    sysno,
   196  					syscall:  syscall,
   197  					expected: retFallthrough,
   198  				})
   199  			}
   200  
   201  			// Add future syscalls.
   202  			for _, sysno := range futureSyscalls {
   203  				baseSysno, err := libseccomp.GetSyscallFromNameByArch("copy_file_range", scmpArch)
   204  				if err != nil {
   205  					t.Fatalf("unknown syscall 'copy_file_range' on arch %q: %v", arch, err)
   206  				}
   207  				sysno += baseSysno
   208  
   209  				syscallTests = append(syscallTests, syscallTest{
   210  					sysno:    sysno,
   211  					syscall:  fmt.Sprintf("syscall_%#x", sysno),
   212  					expected: retErrnoEnosys,
   213  				})
   214  			}
   215  
   216  			// If we're on s390(x) make sure you get -ENOSYS for the "setup"
   217  			// syscall (this is done to work around an issue with s390x's
   218  			// syscall multiplexing which results in unknown syscalls being a
   219  			// setup(2) invocation).
   220  			switch scmpArch {
   221  			case libseccomp.ArchS390, libseccomp.ArchS390X:
   222  				syscallTests = append(syscallTests, syscallTest{
   223  					sysno:    s390xMultiplexSyscall,
   224  					syscall:  "setup",
   225  					expected: retErrnoEnosys,
   226  				})
   227  			}
   228  
   229  			// Test syscalls in the explicit list.
   230  			for _, test := range syscallTests {
   231  				// Override the expected value in the two special cases.
   232  				if !archSet[arch] || isAllowAction(defaultAction) {
   233  					test.expected = retFallthrough
   234  				}
   235  
   236  				payload := mockSyscallPayload(t, test.sysno, nativeArch, 0x1337, 0xF00BA5)
   237  				// NOTE: golang.org/x/net/bpf returns int here rather
   238  				// than uint32.
   239  				rawRet, err := filter.Run(payload)
   240  				if err != nil {
   241  					t.Fatalf("error running filter: %v", err)
   242  				}
   243  				ret := uint32(rawRet)
   244  				if ret != test.expected {
   245  					t.Logf("mock filter for %v %v:", arches, allowedSyscalls)
   246  					for idx, insn := range program {
   247  						t.Logf("  [%4.1d] %s", idx, insn)
   248  					}
   249  					t.Logf("payload: %#v", payload)
   250  					t.Errorf("filter %s(%d) %q(%d): got %#x, want %#x", arch, nativeArch, test.syscall, test.sysno, ret, test.expected)
   251  				}
   252  			}
   253  		}
   254  	}
   255  }
   256  
   257  var testActions = map[string]configs.Action{
   258  	"allow": configs.Allow,
   259  	"log":   configs.Log,
   260  	"errno": configs.Errno,
   261  	"kill":  configs.Kill,
   262  }
   263  
   264  func TestEnosysStub_SingleArch(t *testing.T) {
   265  	for _, arch := range testArches {
   266  		arches := []string{arch}
   267  		t.Run("arch="+arch, func(t *testing.T) {
   268  			for name, action := range testActions {
   269  				t.Run("action="+name, func(t *testing.T) {
   270  					testEnosysStub(t, action, arches)
   271  				})
   272  			}
   273  		})
   274  	}
   275  }
   276  
   277  func TestEnosysStub_MultiArch(t *testing.T) {
   278  	for end := 0; end < len(testArches); end++ {
   279  		for start := 0; start < end; start++ {
   280  			arches := testArches[start:end]
   281  			if len(arches) <= 1 {
   282  				continue
   283  			}
   284  			for _, action := range testActions {
   285  				testEnosysStub(t, action, arches)
   286  			}
   287  		}
   288  	}
   289  }
   290  
   291  func TestDisassembleHugeFilterDoesNotHang(t *testing.T) {
   292  	hugeFilter, err := libseccomp.NewFilter(libseccomp.ActAllow)
   293  	if err != nil {
   294  		t.Fatalf("failed to create seccomp filter: %v", err)
   295  	}
   296  
   297  	for i := 1; i < 10000; i++ {
   298  		if err := hugeFilter.AddRule(libseccomp.ScmpSyscall(i), libseccomp.ActKillThread); err != nil {
   299  			t.Fatalf("failed to add rule to filter %d: %v", i, err)
   300  		}
   301  	}
   302  
   303  	_, err = disassembleFilter(hugeFilter)
   304  	if err != nil {
   305  		t.Fatalf("failed to disassembleFilter: %v", err)
   306  	}
   307  
   308  	// if we exit, we did not hang
   309  }
   310  

View as plain text