summaryrefslogtreecommitdiffstats
path: root/sandbox/linux/seccomp-bpf/sandbox_bpf_unittest.cc
blob: b8ae3b72407fc94883b22cf06043d23f16415bd9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <ostream>

#include "sandbox/linux/seccomp-bpf/sandbox_bpf.h"
#include "sandbox/linux/seccomp-bpf/verifier.h"
#include "testing/gtest/include/gtest/gtest.h"

using namespace playground2;

namespace {

const int kExpectedReturnValue = 42;

TEST(SandboxBpf, CallSupports) {
  // We check that we don't crash, but it's ok if the kernel doesn't
  // support it.
  bool seccomp_bpf_supported =
      Sandbox::supportsSeccompSandbox(-1) == Sandbox::STATUS_AVAILABLE;
  // We want to log whether or not seccomp BPF is actually supported
  // since actual test coverage depends on it.
  RecordProperty("SeccompBPFSupported",
                 seccomp_bpf_supported ? "true." : "false.");
  std::cout << "Seccomp BPF supported: "
            << (seccomp_bpf_supported ? "true." : "false.")
            << "\n";
}

TEST(SandboxBpf, CallSupportsTwice) {
  Sandbox::supportsSeccompSandbox(-1);
  Sandbox::supportsSeccompSandbox(-1);
}

__attribute__((noreturn)) void DoCrash() {
  // Cause a #PF. This only works if we assume that we have the default
  // SIGSEGV handler.
  *(reinterpret_cast<volatile char*>(NULL)) = '\0';
  for (;;) {
    // If we didn't manage to crash there is really nothing we can do reliably
    // but spin.
  }
}

__attribute__((noreturn)) void ExitGroup(int status) {
  syscall(__NR_exit_group, status);
  // If exit_group() failed, there is a high likelihood this
  // happened due to a bug in the sandbox. We therefore cannot
  // blindly assume that the failure happened because we are
  // running on an old kernel that supports exit(), but not
  // exit_group(). We cannot even trust "errno" returning
  // "ENOSYS". So, calling exit() as the fallback for
  // exit_group() would at this point almost certainly be a
  // bug. Instead, try some more aggressive methods to make
  // the program stop.
  DoCrash();
}

// Helper function to start a sandbox with a policy specified in
// evaluator
void StartSandboxOrDie(Sandbox::EvaluateSyscall evaluator) {
  int proc_fd = open("/proc", O_RDONLY|O_DIRECTORY);
  if (proc_fd < 0 || !evaluator) {
    ExitGroup(1);
  }
  if (Sandbox::supportsSeccompSandbox(proc_fd) !=
     Sandbox::STATUS_AVAILABLE) {
    ExitGroup(1);
  }
  Sandbox::setProcFd(proc_fd);
  Sandbox::setSandboxPolicy(evaluator, NULL);
  Sandbox::startSandbox();
}

void RunInSandbox(Sandbox::EvaluateSyscall evaluator,
                      void (*SandboxedCode)()) {
  // TODO(markus): Implement IsEqual for ErrorCode
  // IsEqual(evaluator(__NR_exit_group), Sandbox::SB_ALLOWED) <<
  //    "You need to always allow exit_group() in your test policy";
  StartSandboxOrDie(evaluator);
  // TODO(jln): find a way to use the testing framework inside
  // SandboxedCode() or at the very least to surface errors
  SandboxedCode();
  // SandboxedCode() should have exited, this is a failure
  ExitGroup(1);
}

// evaluator should always allow ExitGroup
// SandboxedCode should ExitGroup(kExpectedReturnValue) if and only if
// things go as expected.
void TryPolicyInProcess(Sandbox::EvaluateSyscall evaluator,
                        void (*SandboxedCode)()) {
  // TODO(jln) figure out a way to surface whether we're actually testing
  // something or not.
  if (Sandbox::supportsSeccompSandbox(-1) == Sandbox::STATUS_AVAILABLE) {
    EXPECT_EXIT(RunInSandbox(evaluator, SandboxedCode),
                ::testing::ExitedWithCode(kExpectedReturnValue),
                "");
  } else {
    // The sandbox is not available. We should still try to exercise what we
    // can.
    // TODO(markus): (crbug.com/141545) let us call the compiler from here.
    Sandbox::setSandboxPolicy(evaluator, NULL);
  }
}

// A simple blacklist test

Sandbox::ErrorCode BlacklistNanosleepPolicy(int sysno) {
  if (sysno < static_cast<int>(MIN_SYSCALL) ||
      sysno > static_cast<int>(MAX_SYSCALL)) {
    // FIXME: we should really not have to do that in a trivial policy
    return ENOSYS;
  }
  switch (sysno) {
    case __NR_nanosleep:
      return EACCES;
    default:
      return Sandbox::SB_ALLOWED;
  }
}

void NanosleepProcess(void) {
  const struct timespec ts = {0, 0};
  errno = 0;
  if(syscall(__NR_nanosleep, &ts, NULL) != -1 || errno != EACCES) {
    ExitGroup(1);
  }
  ExitGroup(kExpectedReturnValue);
}

TEST(SandboxBpf, ApplyBasicBlacklistPolicy) {
  TryPolicyInProcess(BlacklistNanosleepPolicy, NanosleepProcess);
}

// Now do a simple whitelist test

Sandbox::ErrorCode WhitelistGetpidPolicy(int sysno) {
  switch (sysno) {
    case __NR_getpid:
    case __NR_exit_group:
      return Sandbox::SB_ALLOWED;
    default:
      return ENOMEM;
  }
}

void GetpidProcess(void) {
  errno = 0;
  // getpid() should be allowed
  if (syscall(__NR_getpid) < 0 || errno)
    ExitGroup(1);
  // getpgid() should be denied
  if (getpgid(0) != -1 || errno != ENOMEM)
    ExitGroup(1);
  ExitGroup(kExpectedReturnValue);
}

TEST(SandboxBpf, ApplyBasicWhitelistPolicy) {
  TryPolicyInProcess(WhitelistGetpidPolicy, GetpidProcess);
}

// A simple blacklist policy, with a SIGSYS handler

// TODO: provide an API to provide the auxiliary data pointer
// to the evaluator

static int BlacklistNanosleepPolicySigsysAuxData;

intptr_t EnomemHandler(const struct arch_seccomp_data& args, void *aux) {
  // We also check that the auxiliary data is correct
  if (!aux)
    ExitGroup(1);
  *(static_cast<int*>(aux)) = kExpectedReturnValue;
  return -ENOMEM;
}

Sandbox::ErrorCode BlacklistNanosleepPolicySigsys(int sysno) {
  if (sysno < static_cast<int>(MIN_SYSCALL) ||
      sysno > static_cast<int>(MAX_SYSCALL)) {
    // FIXME: we should really not have to do that in a trivial policy
    return ENOSYS;
  }
  switch (sysno) {
    case __NR_nanosleep:
      return Sandbox::ErrorCode(EnomemHandler,
                 static_cast<void *>(&BlacklistNanosleepPolicySigsysAuxData));
    default:
      return Sandbox::SB_ALLOWED;
  }
}

void NanosleepProcessSigsys(void) {
  const struct timespec ts = {0, 0};
  errno = 0;
  // getpid() should work properly
  if (syscall(__NR_getpid) < 0)
    ExitGroup(1);
  // Our Auxiliary Data, should be reset by the signal handler
  BlacklistNanosleepPolicySigsysAuxData = -1;
  errno = 0;
  if (syscall(__NR_nanosleep, &ts, NULL) != -1 || errno != ENOMEM)
    ExitGroup(1);
  // We expect the signal handler to modify AuxData
  if (BlacklistNanosleepPolicySigsysAuxData != kExpectedReturnValue)
    ExitGroup(1);
  else
    ExitGroup(kExpectedReturnValue);
}

TEST(SandboxBpf, BasicBlacklistWithSigsys) {
  TryPolicyInProcess(BlacklistNanosleepPolicySigsys, NanosleepProcessSigsys);
}

// A more complex, but synthetic policy. This tests the correctness of the BPF
// program by iterating through all syscalls and checking for an errno that
// depends on the syscall number. Unlike the Verifier, this exercises the BPF
// interpreter in the kernel.

// We try to make sure we exercise optimizations in the BPF compiler. We make
// sure that the compiler can have an opportunity to coalesce syscalls with
// contiguous numbers and we also make sure that disjoint sets can return the
// same errno.
int SysnoToRandomErrno(int sysno) {
  // Small contiguous sets of 3 system calls return an errno equal to the
  // index of that set + 1 (so that we never return a NUL errno).
  return ((sysno & ~3) >> 2) % 29 + 1;
}

Sandbox::ErrorCode SyntheticPolicy(int sysno) {
  if (sysno < static_cast<int>(MIN_SYSCALL) ||
      sysno > static_cast<int>(MAX_SYSCALL)) {
    // FIXME: we should really not have to do that in a trivial policy.
    return ENOSYS;
  }
  if (sysno == __NR_exit_group) {
    // exit_group() is special, we really need it to work.
    return Sandbox::SB_ALLOWED;
  } else {
    return SysnoToRandomErrno(sysno);
  }
}

void SyntheticProcess(void) {
  // Ensure that that kExpectedReturnValue + syscallnumber + 1 does not int
  // overflow.
  if (std::numeric_limits<int>::max() - kExpectedReturnValue - 1 <
      static_cast<int>(MAX_SYSCALL)) {
    ExitGroup(1);
  }
  for (int syscall_number =  static_cast<int>(MIN_SYSCALL);
           syscall_number <= static_cast<int>(MAX_SYSCALL);
         ++syscall_number) {
    if (syscall_number == __NR_exit_group) {
      // exit_group() is special
      continue;
    }
    errno = 0;
    if (syscall(syscall_number) != -1 ||
        errno != SysnoToRandomErrno(syscall_number)) {
      // Exit with a return value that is different than kExpectedReturnValue
      // to signal an error. Make it easy to see what syscall_number failed in
      // the test report.
      ExitGroup(kExpectedReturnValue + syscall_number + 1);
    }
  }
  ExitGroup(kExpectedReturnValue);
}

TEST(SandboxBpf, SyntheticPolicy) {
  TryPolicyInProcess(SyntheticPolicy, SyntheticProcess);
}

} // namespace