aboutsummaryrefslogtreecommitdiffstats
path: root/openat.cc
blob: ca7e39772f9ae83ee397a649c9372a6f93935907 (plain) (blame)
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#include <string>

#include "capsicum.h"
#include "capsicum-test.h"
#include "syscalls.h"

// Check an open call works and close the resulting fd.
#define EXPECT_OPEN_OK(f) do { \
    int _fd = f;               \
    EXPECT_OK(_fd);            \
    close(_fd);                \
  } while (0)

static void CreateFile(const char *filename, const char *contents) {
  int fd = open(filename, O_CREAT|O_RDWR, 0644);
  EXPECT_OK(fd);
  EXPECT_OK(write(fd, contents, strlen(contents)));
  close(fd);
}

// Test openat(2) in a variety of sitations to ensure that it obeys Capsicum
// "strict relative" rules:
//
// 1. Use strict relative lookups in capability mode or when operating
//    relative to a capability.
// 2. When performing strict relative lookups, absolute paths (including
//    symlinks to absolute paths) are not allowed, nor are paths containing
//    '..' components.
//
// These rules apply when:
//  - the directory FD is a Capsicum capability
//  - the process is in capability mode
//  - the openat(2) operation includes the O_BENEATH flag.
FORK_TEST(Openat, Relative) {
  int etc = open("/etc/", O_RDONLY);
  EXPECT_OK(etc);

  cap_rights_t r_base;
  cap_rights_init(&r_base, CAP_READ, CAP_WRITE, CAP_SEEK, CAP_LOOKUP, CAP_FCNTL, CAP_IOCTL);
  cap_rights_t r_ro;
  cap_rights_init(&r_ro, CAP_READ);
  cap_rights_t r_rl;
  cap_rights_init(&r_rl, CAP_READ, CAP_LOOKUP);

  int etc_cap = dup(etc);
  EXPECT_OK(etc_cap);
  EXPECT_OK(cap_rights_limit(etc_cap, &r_ro));
  int etc_cap_ro = dup(etc);
  EXPECT_OK(etc_cap_ro);
  EXPECT_OK(cap_rights_limit(etc_cap_ro, &r_rl));
  int etc_cap_base = dup(etc);
  EXPECT_OK(etc_cap_base);
  EXPECT_OK(cap_rights_limit(etc_cap_base, &r_base));
#ifdef HAVE_CAP_FCNTLS_LIMIT
  // Also limit fcntl(2) subrights.
  EXPECT_OK(cap_fcntls_limit(etc_cap_base, CAP_FCNTL_GETFL));
#endif
#ifdef HAVE_CAP_IOCTLS_LIMIT
  // Also limit ioctl(2) subrights.
  cap_ioctl_t ioctl_nread = FIONREAD;
  EXPECT_OK(cap_ioctls_limit(etc_cap_base, &ioctl_nread, 1));
#endif

  // openat(2) with regular file descriptors in non-capability mode
  // Should Just Work (tm).
  EXPECT_OPEN_OK(openat(etc, "/etc/passwd", O_RDONLY));
  EXPECT_OPEN_OK(openat(AT_FDCWD, "/etc/passwd", O_RDONLY));
  EXPECT_OPEN_OK(openat(etc, "passwd", O_RDONLY));
  EXPECT_OPEN_OK(openat(etc, "../etc/passwd", O_RDONLY));

  // Lookups relative to capabilities should be strictly relative.
  // When not in capability mode, we don't actually require CAP_LOOKUP.
  EXPECT_OPEN_OK(openat(etc_cap_ro, "passwd", O_RDONLY));
  EXPECT_OPEN_OK(openat(etc_cap_base, "passwd", O_RDONLY));

  // Performing openat(2) on a path with leading slash ignores
  // the provided directory FD.
  EXPECT_OPEN_OK(openat(etc_cap_ro, "/etc/passwd", O_RDONLY));
  EXPECT_OPEN_OK(openat(etc_cap_base, "/etc/passwd", O_RDONLY));
  // Relative lookups that go upward are not allowed.
  EXPECT_OPENAT_FAIL_TRAVERSAL(etc_cap_ro, "../etc/passwd", O_RDONLY);
  EXPECT_OPENAT_FAIL_TRAVERSAL(etc_cap_base, "../etc/passwd", O_RDONLY);

  // A file opened relative to a capability should itself be a capability.
  int fd = openat(etc_cap_base, "passwd", O_RDONLY);
  EXPECT_OK(fd);
  cap_rights_t rights;
  EXPECT_OK(cap_rights_get(fd, &rights));
  EXPECT_RIGHTS_IN(&rights, &r_base);
#ifdef HAVE_CAP_FCNTLS_LIMIT
  cap_fcntl_t fcntls;
  EXPECT_OK(cap_fcntls_get(fd, &fcntls));
  EXPECT_EQ((cap_fcntl_t)CAP_FCNTL_GETFL, fcntls);
#endif
#ifdef HAVE_CAP_IOCTLS_LIMIT
  cap_ioctl_t ioctls[16];
  ssize_t nioctls;
  memset(ioctls, 0, sizeof(ioctls));
  nioctls = cap_ioctls_get(fd, ioctls, 16);
  EXPECT_OK(nioctls);
  EXPECT_EQ(1, nioctls);
  EXPECT_EQ((cap_ioctl_t)FIONREAD, ioctls[0]);
#endif
  close(fd);

  // Enter capability mode; now ALL lookups are strictly relative.
  EXPECT_OK(cap_enter());

  // Relative lookups on regular files or capabilities with CAP_LOOKUP
  // ought to succeed.
  EXPECT_OPEN_OK(openat(etc, "passwd", O_RDONLY));
  EXPECT_OPEN_OK(openat(etc_cap_ro, "passwd", O_RDONLY));
  EXPECT_OPEN_OK(openat(etc_cap_base, "passwd", O_RDONLY));

  // Lookup relative to capabilities without CAP_LOOKUP should fail.
  EXPECT_NOTCAPABLE(openat(etc_cap, "passwd", O_RDONLY));

  // Absolute lookups should fail.
  EXPECT_CAPMODE(openat(AT_FDCWD, "/etc/passwd", O_RDONLY));
  EXPECT_OPENAT_FAIL_TRAVERSAL(etc, "/etc/passwd", O_RDONLY);
  EXPECT_OPENAT_FAIL_TRAVERSAL(etc_cap_ro, "/etc/passwd", O_RDONLY);

  // Lookups containing '..' should fail in capability mode.
  EXPECT_OPENAT_FAIL_TRAVERSAL(etc, "../etc/passwd", O_RDONLY);
  EXPECT_OPENAT_FAIL_TRAVERSAL(etc_cap_ro, "../etc/passwd", O_RDONLY);
  EXPECT_OPENAT_FAIL_TRAVERSAL(etc_cap_base, "../etc/passwd", O_RDONLY);

  fd = openat(etc, "passwd", O_RDONLY);
  EXPECT_OK(fd);

  // A file opened relative to a capability should itself be a capability.
  fd = openat(etc_cap_base, "passwd", O_RDONLY);
  EXPECT_OK(fd);
  EXPECT_OK(cap_rights_get(fd, &rights));
  EXPECT_RIGHTS_IN(&rights, &r_base);
  close(fd);

  fd = openat(etc_cap_ro, "passwd", O_RDONLY);
  EXPECT_OK(fd);
  EXPECT_OK(cap_rights_get(fd, &rights));
  EXPECT_RIGHTS_IN(&rights, &r_rl);
  close(fd);
}

#define TOPDIR "cap_topdir"
#define SUBDIR_ABS TOPDIR "/subdir"
class OpenatTest : public ::testing::Test {
 public:
  // Build a collection of files, subdirs and symlinks:
  //  /tmp/cap_topdir/
  //                 /topfile
  //                 /subdir/
  //                 /subdir/bottomfile
  //                 /symlink.samedir       -> topfile
  //                 /dsymlink.samedir      -> ./
  //                 /symlink.down          -> subdir/bottomfile
  //                 /dsymlink.down         -> subdir/
  //                 /symlink.absolute_in   -> /tmp/cap_topdir/topfile
  //                 /dsymlink.absolute_in  -> /tmp/cap_topdir/
  //                 /symlink.absolute_out  -> /etc/passwd
  //                 /dsymlink.absolute_out -> /etc/
  //                 /symlink.relative_in   -> ../../tmp/cap_topdir/topfile
  //                 /dsymlink.relative_in  -> ../../tmp/cap_topdir/
  //                 /symlink.relative_out  -> ../../etc/passwd
  //                 /dsymlink.relative_out -> ../../etc/
  //                 /subdir/symlink.up     -> ../topfile
  //                 /subdir/dsymlink.up    -> ../
  // (In practice, this is a little more complicated because tmpdir might
  // not be "/tmp".)
  OpenatTest() {
    // Create a couple of nested directories
    int rc = mkdir(TmpFile(TOPDIR), 0755);
    EXPECT_OK(rc);
    if (rc < 0) {
      EXPECT_EQ(EEXIST, errno);
    }
    rc = mkdir(TmpFile(SUBDIR_ABS), 0755);
    EXPECT_OK(rc);
    if (rc < 0) {
      EXPECT_EQ(EEXIST, errno);
    }

    // Figure out a path prefix (like "../..") that gets us to the root
    // directory from TmpFile(TOPDIR).
    const char *p = TmpFile(TOPDIR);  // maybe "/tmp/somewhere/cap_topdir"
    std::string dots2root = "..";
    while (*p++ != '\0') {
      if (*p == '/') {
        dots2root += "/..";
      }
    }

    // Create normal files in each.
    CreateFile(TmpFile(TOPDIR "/topfile"), "Top-level file");
    CreateFile(TmpFile(SUBDIR_ABS "/bottomfile"), "File in subdirectory");

    // Create various symlinks to files.
    EXPECT_OK(symlink("topfile", TmpFile(TOPDIR "/symlink.samedir")));
    EXPECT_OK(symlink("subdir/bottomfile", TmpFile(TOPDIR "/symlink.down")));
    EXPECT_OK(symlink(TmpFile(TOPDIR "/topfile"), TmpFile(TOPDIR "/symlink.absolute_in")));
    EXPECT_OK(symlink("/etc/passwd", TmpFile(TOPDIR "/symlink.absolute_out")));
    std::string dots2top = dots2root + TmpFile(TOPDIR "/topfile");
    EXPECT_OK(symlink(dots2top.c_str(), TmpFile(TOPDIR "/symlink.relative_in")));
    std::string dots2passwd = dots2root + "/etc/passwd";
    EXPECT_OK(symlink(dots2passwd.c_str(), TmpFile(TOPDIR "/symlink.relative_out")));
    EXPECT_OK(symlink("../topfile", TmpFile(SUBDIR_ABS "/symlink.up")));

    // Create various symlinks to directories.
    EXPECT_OK(symlink("./", TmpFile(TOPDIR "/dsymlink.samedir")));
    EXPECT_OK(symlink("subdir/", TmpFile(TOPDIR "/dsymlink.down")));
    EXPECT_OK(symlink(TmpFile(TOPDIR "/"), TmpFile(TOPDIR "/dsymlink.absolute_in")));
    EXPECT_OK(symlink("/etc/", TmpFile(TOPDIR "/dsymlink.absolute_out")));
    std::string dots2cwd = dots2root + tmpdir + "/";
    EXPECT_OK(symlink(dots2cwd.c_str(), TmpFile(TOPDIR "/dsymlink.relative_in")));
    std::string dots2etc = dots2root + "/etc/";
    EXPECT_OK(symlink(dots2etc.c_str(), TmpFile(TOPDIR "/dsymlink.relative_out")));
    EXPECT_OK(symlink("../", TmpFile(SUBDIR_ABS "/dsymlink.up")));

    // Open directory FDs for those directories and for cwd.
    dir_fd_ = open(TmpFile(TOPDIR), O_RDONLY);
    EXPECT_OK(dir_fd_);
    sub_fd_ = open(TmpFile(SUBDIR_ABS), O_RDONLY);
    EXPECT_OK(sub_fd_);
    cwd_ = openat(AT_FDCWD, ".", O_RDONLY);
    EXPECT_OK(cwd_);
    // Move into the directory for the test.
    EXPECT_OK(fchdir(dir_fd_));
  }
  ~OpenatTest() {
    fchdir(cwd_);
    close(cwd_);
    close(sub_fd_);
    close(dir_fd_);
    unlink(TmpFile(SUBDIR_ABS "/symlink.up"));
    unlink(TmpFile(TOPDIR "/symlink.absolute_in"));
    unlink(TmpFile(TOPDIR "/symlink.absolute_out"));
    unlink(TmpFile(TOPDIR "/symlink.relative_in"));
    unlink(TmpFile(TOPDIR "/symlink.relative_out"));
    unlink(TmpFile(TOPDIR "/symlink.down"));
    unlink(TmpFile(TOPDIR "/symlink.samedir"));
    unlink(TmpFile(SUBDIR_ABS "/dsymlink.up"));
    unlink(TmpFile(TOPDIR "/dsymlink.absolute_in"));
    unlink(TmpFile(TOPDIR "/dsymlink.absolute_out"));
    unlink(TmpFile(TOPDIR "/dsymlink.relative_in"));
    unlink(TmpFile(TOPDIR "/dsymlink.relative_out"));
    unlink(TmpFile(TOPDIR "/dsymlink.down"));
    unlink(TmpFile(TOPDIR "/dsymlink.samedir"));
    unlink(TmpFile(SUBDIR_ABS "/bottomfile"));
    unlink(TmpFile(TOPDIR "/topfile"));
    rmdir(TmpFile(SUBDIR_ABS));
    rmdir(TmpFile(TOPDIR));
  }

  // Check openat(2) policing that is common across capabilities, capability mode and O_BENEATH.
  void CheckPolicing(int oflag) {
    // OK for normal access.
    EXPECT_OPEN_OK(openat(dir_fd_, "topfile", O_RDONLY|oflag));
    EXPECT_OPEN_OK(openat(dir_fd_, "subdir/bottomfile", O_RDONLY|oflag));
    EXPECT_OPEN_OK(openat(sub_fd_, "bottomfile", O_RDONLY|oflag));
    EXPECT_OPEN_OK(openat(sub_fd_, ".", O_RDONLY|oflag));

    // Can't open paths with ".." in them.
    EXPECT_OPENAT_FAIL_TRAVERSAL(sub_fd_, "../topfile", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(sub_fd_, "../subdir/bottomfile", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(sub_fd_, "..", O_RDONLY|oflag);

#ifdef HAVE_OPENAT_INTERMEDIATE_DOTDOT
    // OK for dotdot lookups that don't escape the top directory
    EXPECT_OPEN_OK(openat(dir_fd_, "subdir/../topfile", O_RDONLY|oflag));
#endif

    // Check that we can't escape the top directory by the cunning
    // ruse of going via a subdirectory.
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "subdir/../../etc/passwd", O_RDONLY|oflag);

    // Should only be able to open symlinks that stay within the directory.
    EXPECT_OPEN_OK(openat(dir_fd_, "symlink.samedir", O_RDONLY|oflag));
    EXPECT_OPEN_OK(openat(dir_fd_, "symlink.down", O_RDONLY|oflag));
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "symlink.absolute_in", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "symlink.absolute_out", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "symlink.relative_in", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "symlink.relative_out", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(sub_fd_, "symlink.up", O_RDONLY|oflag);

    EXPECT_OPEN_OK(openat(dir_fd_, "dsymlink.samedir/topfile", O_RDONLY|oflag));
    EXPECT_OPEN_OK(openat(dir_fd_, "dsymlink.down/bottomfile", O_RDONLY|oflag));
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "dsymlink.absolute_in/topfile", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "dsymlink.absolute_out/passwd", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "dsymlink.relative_in/topfile", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "dsymlink.relative_out/passwd", O_RDONLY|oflag);
    EXPECT_OPENAT_FAIL_TRAVERSAL(sub_fd_, "dsymlink.up/topfile", O_RDONLY|oflag);

    // Although recall that O_NOFOLLOW prevents symlink following in final component.
    EXPECT_SYSCALL_FAIL(E_TOO_MANY_LINKS, openat(dir_fd_, "symlink.samedir", O_RDONLY|O_NOFOLLOW|oflag));
    EXPECT_SYSCALL_FAIL(E_TOO_MANY_LINKS, openat(dir_fd_, "symlink.down", O_RDONLY|O_NOFOLLOW|oflag));
  }

 protected:
  int dir_fd_;
  int sub_fd_;
  int cwd_;
};

TEST_F(OpenatTest, WithCapability) {
  // Any kind of symlink can be opened relative to an ordinary directory FD.
  EXPECT_OPEN_OK(openat(dir_fd_, "symlink.samedir", O_RDONLY));
  EXPECT_OPEN_OK(openat(dir_fd_, "symlink.down", O_RDONLY));
  EXPECT_OPEN_OK(openat(dir_fd_, "symlink.absolute_in", O_RDONLY));
  EXPECT_OPEN_OK(openat(dir_fd_, "symlink.absolute_out", O_RDONLY));
  EXPECT_OPEN_OK(openat(dir_fd_, "symlink.relative_in", O_RDONLY));
  EXPECT_OPEN_OK(openat(dir_fd_, "symlink.relative_out", O_RDONLY));
  EXPECT_OPEN_OK(openat(sub_fd_, "symlink.up", O_RDONLY));

  // Now make both DFDs into Capsicum capabilities.
  cap_rights_t r_rl;
  cap_rights_init(&r_rl, CAP_READ, CAP_LOOKUP, CAP_FCHDIR);
  EXPECT_OK(cap_rights_limit(dir_fd_, &r_rl));
  EXPECT_OK(cap_rights_limit(sub_fd_, &r_rl));
  CheckPolicing(0);
  // Use of AT_FDCWD is independent of use of a capability.
  // Can open paths starting with "/" against a capability dfd, because the dfd is ignored.
}

FORK_TEST_F(OpenatTest, InCapabilityMode) {
  EXPECT_OK(cap_enter());  // Enter capability mode
  CheckPolicing(0);

  // Use of AT_FDCWD is banned in capability mode.
  EXPECT_CAPMODE(openat(AT_FDCWD, "topfile", O_RDONLY));
  EXPECT_CAPMODE(openat(AT_FDCWD, "subdir/bottomfile", O_RDONLY));
  EXPECT_CAPMODE(openat(AT_FDCWD, "/etc/passwd", O_RDONLY));

  // Can't open paths starting with "/" in capability mode.
  EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "/etc/passwd", O_RDONLY);
  EXPECT_OPENAT_FAIL_TRAVERSAL(sub_fd_, "/etc/passwd", O_RDONLY);
}

#ifdef O_BENEATH
TEST_F(OpenatTest, WithFlag) {
  CheckPolicing(O_BENEATH);

  // Check with AT_FDCWD.
  EXPECT_OPEN_OK(openat(AT_FDCWD, "topfile", O_RDONLY|O_BENEATH));
  EXPECT_OPEN_OK(openat(AT_FDCWD, "subdir/bottomfile", O_RDONLY|O_BENEATH));

  // Can't open paths starting with "/" with O_BENEATH specified.
  EXPECT_OPENAT_FAIL_TRAVERSAL(AT_FDCWD, "/etc/passwd", O_RDONLY|O_BENEATH);
  EXPECT_OPENAT_FAIL_TRAVERSAL(dir_fd_, "/etc/passwd", O_RDONLY|O_BENEATH);
  EXPECT_OPENAT_FAIL_TRAVERSAL(sub_fd_, "/etc/passwd", O_RDONLY|O_BENEATH);
}

FORK_TEST_F(OpenatTest, WithFlagInCapabilityMode) {
  EXPECT_OK(cap_enter());  // Enter capability mode
  CheckPolicing(O_BENEATH);
}
#endif