Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

snap-{seccomp,confine}: rework seccomp denylist #12971

Closed
wants to merge 10 commits into from
43 changes: 35 additions & 8 deletions cmd/snap-confine/seccomp-support.c
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,39 @@ 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",

struct sock_fprog SC_CLEANUP(sc_cleanup_seccomp_profile) prog_allow = { 0 };
sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s/filter.allow",
filter_profile_dir, security_tag);
if (!sc_load_seccomp_profile_path(profile_path, &prog_allow)) {
return false;
}

struct sock_fprog SC_CLEANUP(sc_cleanup_seccomp_profile) prog_deny = { 0 };
sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s/filter.deny",
filter_profile_dir, security_tag);
if (!sc_load_seccomp_profile_path(profile_path, &prog_deny)) {
return false;
}

sc_apply_seccomp_filter(&prog_deny);
sc_apply_seccomp_filter(&prog_allow);

return true;
}

void sc_cleanup_seccomp_profile(struct sock_fprog *prog) {
free(prog->filter);
prog->filter = NULL;
}

bool sc_load_seccomp_profile_path(const char *profile_path, struct sock_fprog *prog)
{
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 Expand Up @@ -165,11 +191,12 @@ bool sc_apply_seccomp_profile_for_security_tag(const char *security_tag)
if (sc_streq(bpf, "@unrestricted\n")) {
return false;
}
struct sock_fprog prog = {
.len = num_read / sizeof(struct sock_filter),
.filter = (struct sock_filter *)bpf,
};
sc_apply_seccomp_filter(&prog);
prog->len = num_read / sizeof(struct sock_filter);
prog->filter = (struct sock_filter *)malloc(num_read);
if (prog->filter == NULL) {
die("cannot allocate %li bytes of memory for seccomp filter ", num_read);
}
memcpy(prog->filter, bpf, num_read);
return true;
}

Expand Down
18 changes: 18 additions & 0 deletions cmd/snap-confine/seccomp-support.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

#include <stdbool.h>

#include <linux/filter.h>

/**
* 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 All @@ -45,6 +47,22 @@
**/
bool sc_apply_seccomp_profile_for_security_tag(const char *security_tag);

/** sc_apply_global_seccomp_profile applies the global seccomp profile
**/
void sc_apply_global_seccomp_profile(void);

/**
* sc_load_seccomp_profile_path loads the seccomp profile from the given
* profile_path into the given sock_fprog. The sock_fprog needs to be
* freed with sc_cleanup_seccomp_profile() later.
*
* The return value indicates if the loading was successful.
**/
bool sc_load_seccomp_profile_path(const char *profile_path, struct sock_fprog *prog);

/**
* sc_cleanup_seccomp_profile frees the dynamic memory from sock_fprog.
**/
void sc_cleanup_seccomp_profile(struct sock_fprog *prog);

#endif
136 changes: 107 additions & 29 deletions cmd/snap-seccomp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,13 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"

"golang.org/x/sys/unix"

"github.com/seccomp/libseccomp-golang"

"github.com/snapcore/snapd/arch"
Expand Down Expand Up @@ -583,11 +586,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 +608,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,27 +794,100 @@ func complainAction() seccomp.ScmpAction {
return seccomp.ActAllow
}

// XXX: find a more elegant way for this
func atomicWriteInDir(targetDir string, f func(stageDir string) error) error {
stageDir := targetDir + ".new"
for _, d := range []string{targetDir, stageDir} {
if err := os.MkdirAll(d, 0755); err != nil {
return err
}
}
defer os.RemoveAll(stageDir)

if err := f(stageDir); err != nil {
return err
}

if err := unix.Renameat2(0, stageDir, 0, targetDir, unix.RENAME_EXCHANGE); err != nil {
return err
}

return nil
}

func writeUnrestrictedFilter(targetDir string) error {
return atomicWriteInDir(targetDir, func(stageDir string) error {
for _, ext := range []string{"filter.allow", "filter.deny"} {
if err := osutil.AtomicWrite(filepath.Join(stageDir, ext), bytes.NewBufferString("@unrestricted\n"), 0644, 0); err != nil {
return err
}
}
return nil
})
}

func writeSeccompFilterFiles(targetDir string, filterAllow, filterDeny *seccomp.ScmpFilter) error {
return atomicWriteInDir(targetDir, func(stageDir string) error {
pathAllow := filepath.Join(stageDir, "filter.allow")
pathDeny := filepath.Join(stageDir, "filter.deny")

for _, d := range []struct {
outFile string
outFilter *seccomp.ScmpFilter
}{
{pathAllow, filterAllow},
{pathDeny, filterDeny},
} {
fout, err := os.Create(d.outFile)
if err != nil {
return err
}
defer fout.Close()

if err := d.outFilter.ExportBPF(fout); err != nil {
return err
}
if err := fout.Sync(); err != nil {
return err
}
}
return nil
})
}

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 {
case unrestricted:
return osutil.AtomicWrite(out, bytes.NewBufferString("@unrestricted\n"), 0644, 0)
return writeUnrestrictedFilter(out)
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 +897,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 +926,14 @@ 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 {
if err := writeSeccompFilterFiles(out, secFilterAllow, secFilterDeny); err != nil {
return err
}
// Cancel once Committed is a NOP
defer fout.Cancel()

if err := secFilter.ExportBPF(fout.File); err != nil {
return err
}
return fout.Commit()
return nil
}

// caches for uid and gid lookups
Expand Down Expand Up @@ -911,7 +989,7 @@ func main() {
switch cmd {
case "compile":
if len(os.Args) < 4 {
fmt.Println("compile needs an input and output file")
fmt.Println("compile needs an input file and output dir")
os.Exit(1)
}
content, err = ioutil.ReadFile(os.Args[2])
Expand Down
Loading
Loading