add seccomp argument filtering #7

Merged
merged 38 commits into from Jun 24, 2016

Conversation

Projects
None yet
3 participants
Contributor

jdstrand commented May 24, 2016

Code changes are from lp:~jdstrand/ubuntu-core-launcher/seccom-arg-filtering
but update for README -> README.md and use of shellcheck

jdstrand added some commits May 24, 2016

add seccomp argument filtering
Code changes are from lp:~jdstrand/ubuntu-core-launcher/seccom-arg-filtering
but update for README -> README.md and use of shellcheck
README.md
-read
-write
-```
+/var/lib/snappy/seccomp/profiles
@zyga

zyga May 27, 2016

Collaborator

This should be /var/lib/snapd/seccomp/profiles

@jdstrand

jdstrand May 31, 2016

Contributor

Fixed committed.

README.md
+
+ SOCKET DOMAIN = ( AF_UNIX | AF_LOCAL | AF_INET | AF_INET6 | AF_IPX |
+ AF_NETLINK | AF_X25 | AF_AX25 | AF_ATMPVC | AF_APPLETALK | AF_PACKET |
+ AF_ALG )
@zyga

zyga May 27, 2016

Collaborator

Can we please add AF_CAN here

@jdstrand

jdstrand Jun 2, 2016

Contributor

Done.

src/seccomp.c
+ scmp_datum_t value = 0;
+ if (strlen(buf_token) == 0) {
+ return PARSE_ERROR;
+ } else if (strlen(buf_token) == 1) {
@zyga

zyga May 27, 2016

Collaborator

Any reason why we're not doing strncmp(buf_token, "=", 1) == 0 here?

@tyhicks

tyhicks May 28, 2016

Collaborator

I agree that we should compare against "=". However, I'd prefer strcmp(buf_token, "=") == 0. The first reason is because we already know that buf_token is NUL terminated because we've already called strcmp() and strlen() on it. The second reason is because strncmp(buf_token, "=", 1) would match "=abcd123" because only the first character is compared.

@jdstrand

jdstrand May 31, 2016

Contributor

I'm not checking for '=' here because '=NNN' is not part of the syntax (you just specify 'NNN' instead). This is checking if the token has a length of 1, parse it as a number and let read_number() set errno. The comment tried to mention that, but it used 'syscall 1'-- I'll update that to use 'syscall N' and adjust the comment to make it more clear.

src/seccomp.c
+ // syscall <=N
+ op = SCMP_CMP_LE;
+ value = read_number(&buf_token[2]);
+ } else if (buf_token[0] == '!') {
@zyga

zyga May 27, 2016

Collaborator

Any reason we are not doing strncmp(buf_token, "!", 1) == 0 here?

@jdstrand

jdstrand May 31, 2016

Contributor

No reason. I agree it would make the code easier to read. Fix committed here and other places you mentioned.

src/seccomp.c
+ // syscall !N
+ op = SCMP_CMP_NE;
+ value = read_number(&buf_token[1]);
+ } else if (buf_token[0] == '>') {
@zyga

zyga May 27, 2016

Collaborator

And here

src/seccomp.c
+ // syscall >N
+ op = SCMP_CMP_GT;
+ value = read_number(&buf_token[1]);
+ } else if (buf_token[0] == '<') {
@zyga

zyga May 27, 2016

Collaborator

And here

+ cat "$TMP"/tmpl >"$TMP"/snap.name.app
+ echo "mbind $i" >>"$TMP"/snap.name.app
+
+ if $L snap.name.app /bin/true 2>/dev/null; then
@zyga

zyga May 27, 2016

Collaborator

I suspect shellcheck will complain about a few of those now.

@jdstrand

jdstrand May 27, 2016

Contributor

I made it all work with shellcheck before performing the PR.

tests/test_bad_seccomp_filter_length
@@ -2,11 +2,11 @@
set -e
-. "$(pwd)/common.sh"
+. "$(pwd)"/common.sh
@zyga

zyga May 27, 2016

Collaborator

Any reason why we're changing this?

@jdstrand

jdstrand May 27, 2016

Contributor

No. It was a cleanup after a sed to fix some shellcheck stuff and I left this change for consistency in other tests.

Collaborator

zyga commented May 27, 2016

A few small comments inline. Two comments here:

  • we should use static functions more often, function names are becoming very generic quickly (map_...)
  • we should do a pass over the whole tree to unify error handling messages ("cannot ....") vs what we do now.
src/seccomp.c
+ // first initialize the htab for our map
+ memset((void *)&map_htab, 0, sizeof(map_htab));
+
+ const int map_length = 82; // one for each map_add
@tyhicks

tyhicks May 28, 2016

Collaborator

Hard-coding 82 will cause a maintenance issue down the road. I'd like to see a struct defined that holds key value pairs (maybe even use an array of ENTRY) and then have an array of structs that are iterated through. sizeof() could be used to determine the size of the array.

@jdstrand

jdstrand Jun 1, 2016

Contributor

@tyhicks - FYI, I went with a linked list of structs. I then populate the htab with the linked list and therefore do not need to hardcode a map length.

src/seccomp.c
+ errno = 0;
+
+ e.key = key;
+ e.data = data;
@tyhicks

tyhicks May 28, 2016

Collaborator

We're abusing e.data here. It is a void pointer and is intended to point to a char array or a struct or something along those lines. We're using a pointer to store a number. It'll work for most cases but we'll run into problems if/when we want to store a data type that takes more space than a pointer requires. One such example is the off_t offset parameter of lseek(). The e.data void pointer wouldn't be able to represent all possible values of an off_t.

Most system call parameters that we'll want to filter can be represented by an off_t so I won't block on this limitation but it at least needs to be clearly documented in the code.

@jdstrand

jdstrand May 31, 2016

Contributor

This was a good catch and as it turns out, we fail the compile on i386 because of our hardening options. I'll rework this so this is no longer an issue.

@jdstrand

jdstrand Jun 2, 2016

Contributor

I've fixed this by allocating a void pointer to hold a pointer to scmp_datum_t. This means that we can reliably use scmp_datum_t everywhere and makes it work on 32bit.

src/seccomp.c
+
+ if (ep == NULL)
+ die("could not initialize map");
+}
@tyhicks

tyhicks May 28, 2016

Collaborator

If we make a typo in the list of calls to map_add() and call it twice with the same key value, hsearch_r() will silently ignore the ENTER operation. Lets be a little proactive here and make sure that doesn't happen. We'll need to die() if ep->data does not equal e.data.

@jdstrand

jdstrand Jun 2, 2016

Contributor

I made this change in the new code with:

    while (p != NULL) {
        errno = 0;
        if (hsearch_r(*p->e, ENTER, &p->ep, &sc_map_htab) == 0)
            die("hsearch_r failed");

        if (&p->ep == NULL)
            die("could not initialize map");
        else {
            scmp_datum_t *d = p->ep->data;
            if (*d != *(scmp_datum_t *) p->e->data)
                die("e->data != ep->data after ENTER");
            printf("ep->data=%lu, e->data=%lu\n", *d, *(scmp_datum_t *) p->e->data);
        }

        p = p->next;
    }

and when I sc_map_add() something twice, hsearch_r doesn't operate the way you describe-- the data is the same.

src/seccomp.c
+ die("could not create map");
+
+ // man 2 socket - domain
+ map_add("AF_UNIX", (void *)AF_UNIX);
@tyhicks

tyhicks May 28, 2016

Collaborator

This can be done in a future commit but "stringification" could clean up these map_add()s and help prevent typos in the future:

https://gcc.gnu.org/onlinedocs/cpp/Stringification.html

#include <stdio.h>
#include <sys/socket.h>

#define kvpair_print(X) kvp(#X, X)

void kvp(const char *key, int value)
{
    printf("%s = %d\n", key, value);
}

int main(void)
{
    kvpair_print(AF_UNIX);
    kvpair_print(SOCK_RAW);
}

That program prints:

$ ./stringification
AF_UNIX = 1
SOCK_RAW = 3

@jdstrand

jdstrand Jun 2, 2016

Contributor

I went ahead and used stringification-- it made the code a lot cleaner and easier to read:

#define sc_map_add(X) sc_map_add_kvp(#X, X)
...
void sc_map_add_kvp(const char *key, scmp_datum_t value)
...
sc_map_add(AF_UNIX);

Note that I am passing scmp_datum_t as the second arg to sc_map_add_kvp(). I think this is ok because it is a widening conversion (eg, AF_UNIX to scmp_datum_t is still '1'). Let me known if I'm missing something.

src/seccomp.c
+ // syscall 1
+ op = SCMP_CMP_EQ;
+ value = read_number(buf_token);
+ } else if (strncmp(buf_token, ">=", 2) == 0) {
@tyhicks

tyhicks May 28, 2016

Collaborator

Lets switch to strcmp() since we know buf_token is NUL terminated and the current test only compares the first two chars and would match ">=ZZZZ".

src/seccomp.c
+ // syscall >=N
+ op = SCMP_CMP_GE;
+ value = read_number(&buf_token[2]);
+ } else if (strncmp(buf_token, "<=", 2) == 0) {
@tyhicks

tyhicks May 28, 2016

Collaborator

Same here. strncmp() -> strcmp()

Contributor

jdstrand commented May 31, 2016

@tyhicks - regarding strncmp vs strcmp, this is because the syntax does not have a space between the operator and the operand. Ie, these are valid: >5 and <=10 but this is not >= 1. As such, the parsing is checking what the string starts with and if it matches, sends the rest of the string to read_number() and lets it handle setting errno.

jdstrand added some commits May 31, 2016

enable tests on i386
refactor required syscall list into tests/common.sh and call that whenever we
define a filter list

update tests/common.sh to run on either amd64 or i386
Collaborator

zyga commented Jun 1, 2016

Please merge with master to use autoconf/automake for this.

jdstrand added some commits Jun 1, 2016

don't hard code the map length for hcreate_r
- Create a sc_map_entry struct with ENTRY item and ENTRY retval for use in a
  linked list.
- Create sc_map_list struct to store the list and the number of items in the
  linked list.
- adjust sc_map_add() to create list nodes and add to the linked list
- adjust sc_map_init() to initialize the linked list and after calling
  sc_map_add() (like before), initialize the htab with the contents of the
  linked list

TODO: adjust sc_map_destroy()
make sc_map_add_kvp() actually work with additional cleanups
- remove unused math.h #include
- properly obtain value in sc_map_search()
- sc_map_add_kvp() allocates a void pointer for e->data
- sc_map_destroy(): free the aforementioned void pointer
Contributor

jdstrand commented Jun 2, 2016

Ok, I think I addressed everyone's comments. Thanks for the reviews so far!

src/seccomp_utils.c
+void sc_map_add_kvp(const char *key, scmp_datum_t value)
+{
+ struct sc_map_entry *node;
+ node = (struct sc_map_entry *)malloc(sizeof(struct sc_map_entry));
@tyhicks

tyhicks Jun 6, 2016

Collaborator

malloc(sizeof(struct foo)) is somewhat dangerous of someone ever changes the type of *node in the future.

malloc(sizeof(*node)) is the safer option.

Also, I find it surprising that you need to cast the return value. Please drop that, if possible.

@jdstrand

jdstrand Jun 10, 2016

Contributor

Done.

src/seccomp_utils.c
+
+ if (ep != NULL) {
+ scmp_datum_t *p = ep->data;
+ val = *p;
@tyhicks

tyhicks Jun 6, 2016

Collaborator

Optional suggestion: remove the declaration of *p and just do val = *(ep->data)

@jdstrand

jdstrand Jun 10, 2016

Contributor

This yields a compiler error but I can use val = *((scmp_datum_t *) ep->data);

src/seccomp_utils.c
+ if (node->e->key == NULL)
+ die("Out of memory creating e->key");
+
+ node->e->data = (void *)malloc(sizeof(scmp_datum_t));
@tyhicks

tyhicks Jun 6, 2016

Collaborator

and here

@jdstrand

jdstrand Jun 10, 2016

Contributor

Done (with node->e->data = malloc(sizeof(scmp_datum_t *));)

@tyhicks

tyhicks Jun 21, 2016

Collaborator

This is incorrect and shows the potential problem. This is allocating size of a pointer instead of the size of the type being used to store the data. But ignore this for now because I think I have a better fix to suggest in a higher-level comment.

src/seccomp_utils.c
+ if (node == NULL)
+ die("Out of memory creating sc_map_entries");
+
+ node->e = (ENTRY *) malloc(sizeof(ENTRY));
@tyhicks

tyhicks Jun 6, 2016

Collaborator

Same comments here as the other malloc() above.

@jdstrand

jdstrand Jun 10, 2016

Contributor

Done.

src/seccomp_utils.c
+ node->e->data = (void *)malloc(sizeof(scmp_datum_t));
+ if (node->e->data == NULL)
+ die("Out of memory creating e->data");
+ *(scmp_datum_t *) node->e->data = value;
@tyhicks

tyhicks Jun 6, 2016

Collaborator

This casting is really odd since it is on the left side of the assignment. It is covering up the fact that this PR has the same problem as the first PR.

node->e->data is of type (void *). value is of type scmp_datum_t which, according to libseccomp's seccomp.h, is a uint64_t:

typedef uint64_t scmp_datum_t;

We're still assigning a 64 bit type to a small type.

@tyhicks

tyhicks Jun 6, 2016

Collaborator

You'll have to declare a new struct, which contains a scmp_datum_t field. You'll assign a pointer to that struct to ENTRY.data when adding to the map.

@jdstrand

jdstrand Jun 10, 2016

Contributor

I think there is some confusion on what I'm doing here. Consider:

node->e->data = malloc(sizeof(scmp_datum_t *));
if (node->e->data == NULL)
        die("Out of memory creating e->data");
*(scmp_datum_t *) node->e->data = value;

What I'm doing is malloc()ing something the size of a scmp_datum_t and assigning the address of that in node->e->data, which is a void pointer. Then I dereference node->e->data and assign it the variable value. Since node->e->data is a void pointer, the (scmp_datum_t *) is used on the left hand sign to say that node->e->data is a pointer to scmp_datum_t, and the * in front says to dereference that pointer. In this manner, we are not assigning a 64 bit type to a small type. node->e->data = malloc(sizeof(scmp_datum_t *)) is pointer to pointer, and *(scmp_datum_t *) node->e->data = value is 64 bit to 64 bit, therefore the sizes are all correct, and we can demonstrate how sc_map_search() later will get the correct value and that the sizes are correct in the attached file. Here are the relevant bits:

scmp_datum_t get_val(ENTRY *e) {
    return *(scmp_datum_t *) e->data;
}

int main(int argc, char *argv[]) {
    ...
    scmp_datum_t value = 4294967297; // bigger than 2^32
    ...
    node->e->data = malloc(sizeof(scmp_datum_t *));
    ...
    *(scmp_datum_t *) node->e->data = value;

    printf("sizeof(&value)=\t\t\t\t\t%u\n", sizeof(&value));
    printf("sizeof(value)=\t\t\t\t\t%u\n", sizeof(value));
    printf("value=\t\t\t\t\t\t%llu\n", value);

    printf("sizeof(node->e->data)=\t\t\t\t%u\n", sizeof(node->e->data));
    printf("sizeof(*(scmp_datum_t *) node->e->data)=\t%u\n", sizeof(*(scmp_datum_t *) node->e->data));
    printf("*(scmp_datum_t *)node->e->data=\t\t\t%llu\n", *(scmp_datum_t *)node->e->data);

    printf("get_val(node->e)=\t\t\t\t%llu\n", get_val(node->e));

Now run the program on a 32 bit system (eg, an i386 chroot) since it shows the different sizes:

$ gcc ./prove-sizes.c && ./a.out
sizeof(&value)=                             4
sizeof(value)=                              8
value=                                      4294967297
sizeof(node->e->data)=                      4
sizeof(*(scmp_datum_t *) node->e->data)=    8
*(scmp_datum_t *)node->e->data=             4294967297
get_val(node->e)=                           4294967297
@jdstrand

jdstrand Jun 10, 2016

Contributor

Meh, I couldn't get github to attach the file. Here it is:

$ cat ./prove-sizes.c
#include <stdio.h>
#include <stdlib.h>
#include <seccomp.h>
#include <search.h>

struct sc_map_entry {
    ENTRY *e;
    ENTRY *ep;
    struct sc_map_entry *next;
};

scmp_datum_t get_val(ENTRY *e) {
    return *(scmp_datum_t *) e->data;
}

int main(int argc, char *argv[])
{
    scmp_datum_t value = 4294967297; // bigger than 2^32

    struct sc_map_entry *node;
    node = malloc(sizeof(*node));
    if (node == NULL)
        perror("no memory");

    node->e = malloc(sizeof(node->e));
    if (node->e == NULL)
        perror("no memory");

    node->e->data = malloc(sizeof(scmp_datum_t *));
    if (node->e->data == NULL)
        perror("no memory");
    *(scmp_datum_t *) node->e->data = value;

    printf("sizeof(&value)=\t\t\t\t\t%u\n", sizeof(&value));
    printf("sizeof(value)=\t\t\t\t\t%u\n", sizeof(value));
    printf("value=\t\t\t\t\t\t%llu\n", value);

    printf("sizeof(node->e->data)=\t\t\t\t%u\n", sizeof(node->e->data));
    printf("sizeof(*(scmp_datum_t *) node->e->data)=\t%u\n", sizeof(*(scmp_datum_t *) node->e->data));
    printf("*(scmp_datum_t *)node->e->data=\t\t\t%llu\n", *(scmp_datum_t *)node->e->data);

    printf("get_val(node->e)=\t\t\t\t%llu\n", get_val(node->e));
    return 0;
}
@tyhicks

tyhicks Jun 21, 2016

Collaborator

Running this program through valgrind shows quite a few issues (remove the printf()s first because incorrect modifiers are used that generate additional compiler and valgrind warnings).

Collaborator

tyhicks commented Jun 6, 2016

I'm going to hold off on reviewing the rest since changes to the core of this PR will need to change.

Collaborator

zyga commented Jun 6, 2016

Thank you for catching that @tyhicks

Contributor

jdstrand commented Jun 10, 2016

At this point I made the few small requested cleanups and shown how the assignments are correct. This should be ready to review by @tyhicks (but I'm going to start merging in all of Zygmunt's changes so it is commitable again.

Contributor

jdstrand commented Jun 10, 2016

Actually, I'm going to hold off on those merges so everything is clear when Tyler is ready to review again.

tyhicks added some commits Jun 21, 2016

Alloc enough mem to hold the object that node->e points to
Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Make a copy of the value variable without casting
Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Alloc the right amount of mem even if the type of sc_map_entries changes
Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Collaborator

tyhicks commented Jun 21, 2016

There are a few small changes that I'd like to see. Rather than drag this PR out any longer, I proposed them in jdstrand#1.

With those changes, this looks ok to me.

Collaborator

tyhicks commented on 7902948 Jun 22, 2016

Looks good!

Contributor

jdstrand commented Jun 22, 2016

Note that for some reason the testsuite doesn't run if I go into an i386 schroot, install all the build-deps and then do:

$ debian/rules build

This is not a regression in this PR. If I go into an amd64 schroot and do:

$ debian/rules build

then go into an i386 chroot and do:

$ make clean
$ make check

the tests pass on i386. Note that in the past that the testsuite failed on i386, but I fixed that in this PR since having 32 bit and 64 bit testsuite runs is useful. Perhaps 32 bit was disabled somewhere else with the change to autoconf?

Collaborator

tyhicks commented Jun 22, 2016

@jdstrand looks like tests were disable on all architectures except for amd64: 02205bc

Contributor

jdstrand commented Jun 22, 2016

Thanks @tyhicks that was helpful. Re-enabled tests on i686.

Contributor

jdstrand commented Jun 22, 2016

@zyga - this is ready to land now. If you have instructions on how to do more testing with a snapd that uses snap-confine, I could do some more testing.

Collaborator

zyga commented Jun 22, 2016

@jdstrand just install the package on your system.

Contributor

jdstrand commented Jun 22, 2016

Verified that seccomp correctly:

  • blocks setpriority when it isn't in the policy
  • allows setpriority when it is
  • blocks setpriority when args are specified in the policy that don't match the caller
  • allows setpriority when args are specified in the policy that match the caller
src/seccomp-support.c
+ if (ep != NULL)
+ val = ep->data;
+ else
+ errno = EINVAL;
@zyga

zyga Jun 23, 2016

Collaborator

And we crash because val is NULL and gets dereferenced below.

@jdstrand

jdstrand Jun 23, 2016

Contributor

Meh, this was introduced in the last code commit (7902948). Nice catch!

src/seccomp-support.c
+}
+
+/* Caller must check if errno != 0 */
+scmp_datum_t read_number(char *s)
@zyga

zyga Jun 23, 2016

Collaborator

This function should be static

src/seccomp-support.c
+ return val;
+}
+
+int parse_line(char *line, struct seccomp_args *sargs)
@zyga

zyga Jun 23, 2016

Collaborator

This function should be static

Contributor

jdstrand commented Jun 23, 2016

Thanks for the review @zyga. I made the requested changes and also made the sc_map_* functions static. They are almost generalized, but sc_map_init() would need work to be used outside of this file.

Contributor

jdstrand commented Jun 23, 2016

retest this please

Collaborator

zyga commented Jun 24, 2016

Hmm, I need to adjust the secret handling in snap-confine to let tests pass for forks

Collaborator

zyga commented Jun 24, 2016

I'm merging this. I don't want to delay on travis config

@zyga zyga merged commit 5443af4 into snapcore:master Jun 24, 2016

1 check failed

continuous-integration/travis-ci/pr The Travis CI build failed
Details

@jdstrand jdstrand deleted the jdstrand:seccomp-arg-filtering branch Jun 29, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment