Skip to content

Commit

Permalink
snap-{seccomp,confine}: rework seccomp denylist
Browse files Browse the repository at this point in the history
When a denylist was introduced in PR#12849 we reached the limits
of the API of libseccomp and some things are now hard to reason
about [1]. This is mostly because of the way libseccomp is working.

So this commit changes the approach and instead of trying to use
a single filter the work is split into two filters:
1. explicit allow list
2. explicit deny list

and then load both filters into the kernel. The way the kernel
works is that it selects the most restrictive action.

So in the case of PR#12849:
```
~ioctl - TIOCSTI
~ioctl - TIOCLINUX
ioctl
```
For `ioctl(TIOCLINUX)` the first allow filter would pass `ioctl`
but the second deny filter would correctly deny the TIOCLINUX.

[1] canonical#12849 (comment)
  • Loading branch information
mvo5 committed Jul 10, 2023
1 parent 18f82a8 commit 8b106ad
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 71 deletions.
18 changes: 15 additions & 3 deletions cmd/snap-confine/seccomp-support.c
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,25 @@ static void validate_bpfpath_is_safe(const char *path)
}
}

bool sc_apply_seccomp_profile_for_security_tag(const char *security_tag)
{
bool sc_apply_seccomp_profile_for_security_tag(const char *security_tag) {
debug("loading bpf program for security tag %s", security_tag);

char profile_path[PATH_MAX] = { 0 };
sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s.bin",

sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s.bin.allow",
filter_profile_dir, security_tag);
if (!sc_apply_seccomp_profile_path(profile_path)) {
return false;
}

sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s.bin.deny",
filter_profile_dir, security_tag);
return sc_apply_seccomp_profile_path(profile_path);
}

bool sc_apply_seccomp_profile_path(const char *profile_path)
{
debug("loading bpf program from file %s", profile_path);

// Wait some time for the security profile to show up. When
// the system boots snapd will created security profiles, but
Expand Down
1 change: 1 addition & 0 deletions cmd/snap-confine/seccomp-support.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include <stdbool.h>

bool sc_apply_seccomp_profile_path(const char *profile_path);
/**
* sc_apply_seccomp_profile_for_security_tag applies a seccomp profile to the
* current process. The filter is loaded from a pre-compiled bpf bytecode
Expand Down
87 changes: 60 additions & 27 deletions cmd/snap-seccomp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,11 +583,12 @@ var (
errnoOnImplicitDenial int16 = C.EPERM
)

func parseLine(line string, secFilter *seccomp.ScmpFilter) error {
func parseLine(line string, secFilterAllow, secFilterDeny *seccomp.ScmpFilter) error {
// ignore comments and empty lines
if strings.HasPrefix(line, "#") || line == "" {
return nil
}
secFilter := secFilterAllow

// regular line
tokens := strings.Fields(line)
Expand All @@ -604,6 +605,7 @@ func parseLine(line string, secFilter *seccomp.ScmpFilter) error {
if strings.HasPrefix(syscallName, "~") {
action = seccomp.ActErrno.SetReturnCode(errnoOnExplicitDenial)
syscallName = syscallName[1:]
secFilter = secFilterDeny
}

secSyscall, err := seccomp.GetSyscallFromName(syscallName)
Expand Down Expand Up @@ -789,9 +791,24 @@ func complainAction() seccomp.ScmpAction {
return seccomp.ActAllow
}

func newComplainFilter(complainAct seccomp.ScmpAction) (*seccomp.ScmpFilter, error) {
secFilter, err := seccomp.NewFilter(complainAct)
if err != nil {
if complainAct != seccomp.ActAllow {
// ActLog is only supported in newer versions
// of the kernel, libseccomp, and
// libseccomp-golang. Attempt to fall back to
// ActAllow before erroring out.
complainAct = seccomp.ActAllow
secFilter, err = seccomp.NewFilter(complainAct)
}
}
return secFilter, err
}

func compile(content []byte, out string) error {
var err error
var secFilter *seccomp.ScmpFilter
var secFilterAllow, secFilterDeny *seccomp.ScmpFilter

unrestricted, complain := preprocess(content)
switch {
Expand All @@ -800,16 +817,13 @@ func compile(content []byte, out string) error {
case complain:
var complainAct seccomp.ScmpAction = complainAction()

secFilter, err = seccomp.NewFilter(complainAct)
secFilterAllow, err = newComplainFilter(complainAct)
if err != nil {
if complainAct != seccomp.ActAllow {
// ActLog is only supported in newer versions
// of the kernel, libseccomp, and
// libseccomp-golang. Attempt to fall back to
// ActAllow before erroring out.
complainAct = seccomp.ActAllow
secFilter, err = seccomp.NewFilter(complainAct)
}
return fmt.Errorf("cannot create seccomp filter: %s", err)
}
secFilterDeny, err = newComplainFilter(complainAct)
if err != nil {
return fmt.Errorf("cannot create seccomp filter: %s", err)
}

// Set unrestricted to 'true' to fallback to the pre-ActLog
Expand All @@ -819,19 +833,26 @@ func compile(content []byte, out string) error {
unrestricted = true
}
default:
secFilter, err = seccomp.NewFilter(seccomp.ActErrno.SetReturnCode(errnoOnImplicitDenial))
secFilterAllow, err = seccomp.NewFilter(seccomp.ActErrno.SetReturnCode(errnoOnImplicitDenial))
if err != nil {
return fmt.Errorf("cannot create seccomp filter: %s", err)
}
secFilterDeny, err = seccomp.NewFilter(seccomp.ActAllow)
if err != nil {
return fmt.Errorf("cannot create seccomp filter: %s", err)
}
}
if err != nil {
return fmt.Errorf("cannot create seccomp filter: %s", err)
if err := addSecondaryArches(secFilterAllow); err != nil {
return err
}
if err := addSecondaryArches(secFilter); err != nil {
if err := addSecondaryArches(secFilterDeny); err != nil {
return err
}

if !unrestricted {
scanner := bufio.NewScanner(bytes.NewBuffer(content))
for scanner.Scan() {
if err := parseLine(scanner.Text(), secFilter); err != nil {
if err := parseLine(scanner.Text(), secFilterAllow, secFilterDeny); err != nil {
return fmt.Errorf("cannot parse line: %s", err)
}
}
Expand All @@ -841,21 +862,33 @@ func compile(content []byte, out string) error {
}

if osutil.GetenvBool("SNAP_SECCOMP_DEBUG") {
secFilter.ExportPFC(os.Stdout)
secFilterAllow.ExportPFC(os.Stdout)
secFilterDeny.ExportPFC(os.Stdout)
}

// write atomically
fout, err := osutil.NewAtomicFile(out, 0644, 0, osutil.NoChown, osutil.NoChown)
if err != nil {
return err
}
// Cancel once Committed is a NOP
defer fout.Cancel()

if err := secFilter.ExportBPF(fout.File); err != nil {
return err
for _, ext := range []string{".allow", ".deny"} {
fout, err := osutil.NewAtomicFile(out+ext, 0644, 0, osutil.NoChown, osutil.NoChown)
if err != nil {
return err
}
// Cancel once Committed is a NOP
defer fout.Cancel()

switch ext {
case ".allow":
err = secFilterAllow.ExportBPF(fout.File)
case ".deny":
err = secFilterDeny.ExportBPF(fout.File)
}
if err != nil {
return err
}
if err := fout.Commit(); err != nil {
return err
}
}
return fout.Commit()
return nil
}

// caches for uid and gid lookups
Expand Down
64 changes: 49 additions & 15 deletions cmd/snap-seccomp/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,45 @@ var seccompBpfLoaderContent = []byte(`
#define MAX_BPF_SIZE 32 * 1024
int sc_apply_seccomp_bpf(const char* profile_path)
int sc_apply_seccomp_bpf(const char* profile_path_allow, const char* profile_path_deny)
{
unsigned char bpf[MAX_BPF_SIZE + 1]; // account for EOF
unsigned char bpf_allow[MAX_BPF_SIZE + 1]; // account for EOF
unsigned char bpf_deny[MAX_BPF_SIZE + 1]; // account for EOF
FILE* fp;
fp = fopen(profile_path, "rb");
// allow
fp = fopen(profile_path_allow, "rb");
if (fp == NULL) {
fprintf(stderr, "cannot read %s\n", profile_path_allow);
return -1;
}
// set 'size' to 1; to get bytes transferred
size_t num_read = fread(bpf_allow, 1, sizeof(bpf_allow), fp);
if (ferror(fp) != 0) {
perror("fread()");
return -1;
} else if (feof(fp) == 0) {
fprintf(stderr, "file too big\n");
return -1;
}
fclose(fp);
struct sock_fprog prog_allow = {
.len = num_read / sizeof(struct sock_filter),
.filter = (struct sock_filter*)bpf_allow,
};
// deny
fp = fopen(profile_path_deny, "rb");
if (fp == NULL) {
fprintf(stderr, "cannot read %s\n", profile_path);
fprintf(stderr, "cannot read %s\n", profile_path_deny);
return -1;
}
// set 'size' to 1; to get bytes transferred
size_t num_read = fread(bpf, 1, sizeof(bpf), fp);
num_read = fread(bpf_deny, 1, sizeof(bpf_deny), fp);
if (ferror(fp) != 0) {
perror("fread()");
Expand All @@ -93,9 +120,9 @@ int sc_apply_seccomp_bpf(const char* profile_path)
}
fclose(fp);
struct sock_fprog prog = {
struct sock_fprog prog_deny = {
.len = num_read / sizeof(struct sock_filter),
.filter = (struct sock_filter*)bpf,
.filter = (struct sock_filter*)bpf_deny,
};
// Set NNP to allow loading seccomp policy into the kernel without
Expand All @@ -105,26 +132,32 @@ int sc_apply_seccomp_bpf(const char* profile_path)
return -1;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...) failed");
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog_deny)) {
perror("prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...) deny failed");
return -1;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog_allow)) {
perror("prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...) allow failed");
return -1;
}
return 0;
}
int main(int argc, char* argv[])
{
int rc = 0;
if (argc < 2) {
fprintf(stderr, "Usage: %s <bpf file> [prog ...]\n", argv[0]);
fprintf(stderr, "Usage: %s <bpf file-allow> <bpf-file-deny> [prog ...]\n", argv[0]);
return 1;
}
rc = sc_apply_seccomp_bpf(argv[1]);
// allow, deny
rc = sc_apply_seccomp_bpf(argv[1], argv[2]);
if (rc != 0)
return -rc;
execv(argv[2], (char* const*)&argv[2]);
execv(argv[3], (char* const*)&argv[3]);
perror("execv failed");
return 1;
}
Expand Down Expand Up @@ -333,7 +366,7 @@ mprotect
}
}

cmd := exec.Command(s.seccompBpfLoader, bpfPath, syscallRunner, syscallRunnerArgs[0], syscallRunnerArgs[1], syscallRunnerArgs[2], syscallRunnerArgs[3], syscallRunnerArgs[4], syscallRunnerArgs[5], syscallRunnerArgs[6])
cmd := exec.Command(s.seccompBpfLoader, bpfPath+".allow", bpfPath+".deny", syscallRunner, syscallRunnerArgs[0], syscallRunnerArgs[1], syscallRunnerArgs[2], syscallRunnerArgs[3], syscallRunnerArgs[4], syscallRunnerArgs[5], syscallRunnerArgs[6])
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
Expand Down Expand Up @@ -395,8 +428,9 @@ func (s *snapSeccompSuite) TestCompile(c *C) {
{"read", "read", Allow},
{"read\nwrite\nexecve\n", "write", Allow},

// trivial denial
{"read", "ioctl", Deny},
// trivial denial (uses write in allow-list to ensure any
// errors printing is visible)
{"write", "ioctl", Deny},

// test argument filtering syntax, we currently support:
// >=, <=, !, <, >, |
Expand Down
9 changes: 1 addition & 8 deletions interfaces/seccomp/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,7 @@ inotify_rm_watch
# TODO: this should be scaled back even more
~ioctl - TIOCSTI
~ioctl - TIOCLINUX
# restrict argument otherwise will match all uses of ioctl() and allow the rules
# that were disallowed above
# TODO: Fix the need to keep TIOCLINUX here - the issue is a unrestricted
# allow for "ioctl" here makes libseccomp "optimize" the deny rules
# above away and the generated bpf becomes just "allow ioctl".
# We should fix this by creating a way to make "AND" rules, so
# this becomes "ioctl - !TIOCSTI&&!TIOCLINUX" and remove the "~" again.
ioctl - !TIOCSTI
ioctl
io_cancel
io_destroy
Expand Down
Loading

0 comments on commit 8b106ad

Please sign in to comment.