apparmor/parser/af_unix.cc
John Johansen dd44858e60 parser: first step implementing fine grained mediation for unix domain sockets
This patch implements parsing of fine grained mediation for unix domain
sockets, that have abstract and anonymous paths. Sockets with file
system paths are handled by regular file access rules.

The unix network rules follow the general fine grained network
rule pattern of

  [<qualifiers>] af_name [<access expr>] [<rule conds>] [<local expr>] [<peer expr>]

specifically for af_unix this is

  [<qualifiers>] 'unix' [<access expr>] [<rule conds>] [<local expr>] [<peer expr>]

  <qualifiers> = [ 'audit' ] [ 'allow' | 'deny' ]

  <access expr> = ( <access> | <access list> )

  <access> = ( 'server' | 'create' | 'bind' | 'listen' | 'accept' |
               'connect' | 'shutdown' | 'getattr' | 'setattr' |
	       'getopt' | 'setopt' |
               'send' | 'receive' | 'r' | 'w' | 'rw' )
  (some access modes are incompatible with some rules or require additional
   parameters)

  <access list> = '(' <access> ( [','] <WS> <access> )* ')'

  <WS> = white space

  <rule conds> = ( <type cond> | <protocol cond> )*
     each cond can appear at most once

  <type cond> = 'type' '='  ( <AARE> | '(' ( '"' <AARE> '"' | <AARE> )+ ')' )

  <protocol cond> = 'protocol' '='  ( <AARE> | '(' ( '"' <AARE> '"' | <AARE> )+ ')' )

  <local expr> = ( <path cond> | <attr cond> | <opt cond> )*
     each cond can appear at most once

  <peer expr> = 'peer' '=' ( <path cond> | <label cond> )+
     each cond can appear at most once

  <path cond> = 'path' '=' ( <AARE> | '(' '"' <AARE> '"' | <AARE> ')' )

  <label cond> = 'label' '=' ( <AARE> | '(' '"' <AARE> '"' | <AARE> ')')

  <attr cond> = 'attr' '=' ( <AARE> | '(' '"' <AARE> '"' | <AARE> ')' )

  <opt cond> = 'opt' '=' ( <AARE> | '(' '"' <AARE> '"' | <AARE> ')' )

  <AARE> = ?*[]{}^ ( see man page )

 unix domain socket rules are accumulated so that the granted unix
 socket permissions are the union of all the listed unix rule permissions.

 unix domain socket rules are broad and general and become more restrictive
 as further information is specified. Policy may be specified down to
 the path and label level. The content of the communication is not
 examined.

 Some permissions are not compatible with all unix rules.

 unix socket rule permissions are implied when a rule does not explicitly
 state an access list. By default if a rule does not have an access list
 all permissions that are compatible with the specified set of local
 and peer conditionals are implied.

 The 'server', 'r', 'w' and 'rw' permissions are aliases for other permissions.
 server = (create, bind, listen, accept)
 r = (receive, getattr, getopt)
 w = (create, connect, send, setattr, setopt)

In addition it supports the v7 kernel abi semantics around generic
network rules. The v7 abi removes the masking unix and netlink
address families from the generic masking and uses fine grained
mediation for an address type if supplied.

This means that the rules

  network unix,
  network netlink,

are now enforced instead of ignored. The parser previously could accept
these but the kernel would ignore anything written to them. If a network
rule is supplied it takes precedence over the finer grained mediation
rule. If permission is not granted via a broad network access rule
fine grained mediation is applied.

Signed-off-by: John Johansen <john.johansen@canonical.com>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
2014-09-03 13:22:26 -07:00

386 lines
9.8 KiB
C++

/*
* Copyright (c) 2014
* Canonical, Ltd. (All rights reserved)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public
* License published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc. or Canonical
* Ltd.
*/
#include <stdlib.h>
#include <string.h>
#include <sys/apparmor.h>
#include <iomanip>
#include <string>
#include <iostream>
#include <sstream>
#include "network.h"
#include "parser.h"
#include "profile.h"
#include "af_unix.h"
int parse_unix_mode(const char *str_mode, int *mode, int fail)
{
return parse_X_mode("unix", AA_VALID_NET_PERMS, str_mode, mode, fail);
}
static struct supported_cond supported_conds[] = {
{ "path", true, false, false, either_cond },
{ NULL, false, false, false, local_cond }, /* sentinal */
};
void unix_rule::move_conditionals(struct cond_entry *conds)
{
struct cond_entry *ent;
list_for_each(conds, ent) {
if (!cond_check(supported_conds, ent, false, "unix") &&
!move_base_cond(ent, false)) {
yyerror("unix rule: invalid conditional '%s'\n",
ent->name);
continue;
}
if (strcmp(ent->name, "path") == 0) {
move_conditional_value("unix socket", &path, ent);
if (path[0] != '@' && strcmp(path, "none") != 0)
yyerror("unix rule: invalid value for path='%s'\n", path);
}
/* TODO: add conditionals for
* listen queue length
* attrs that can be read/set
* ops that can be read/set
* allow in on
* type, protocol
* local label match, and set
*/
}
}
void unix_rule::move_peer_conditionals(struct cond_entry *conds)
{
struct cond_entry *ent;
list_for_each(conds, ent) {
if (!cond_check(supported_conds, ent, true, "unix") &&
!move_base_cond(ent, true)) {
yyerror("unix rule: invalid peer conditional '%s'\n",
ent->name);
continue;
}
if (strcmp(ent->name, "path") == 0) {
move_conditional_value("unix", &peer_path, ent);
if (peer_path[0] != '@' && strcmp(path, "none") != 0)
yyerror("unix rule: invalid value for path='%s'\n", peer_path);
}
}
}
unix_rule::unix_rule(unsigned int type_p, bool audit_p, bool denied):
af_rule("unix"), path(NULL), peer_path(NULL)
{
if (type_p != 0xffffffff) {
sock_type_n = type_p;
sock_type = strdup(net_find_type_name(type_p));
if (!sock_type)
yyerror("socket rule: invalid socket type '%d'", type_p);
}
mode = AA_VALID_NET_PERMS;
audit = audit_p ? AA_VALID_NET_PERMS : 0;
deny = denied;
}
unix_rule::unix_rule(int mode_p, struct cond_entry *conds,
struct cond_entry *peer_conds):
af_rule("unix"), path(NULL), peer_path(NULL)
{
move_conditionals(conds);
move_peer_conditionals(peer_conds);
if (mode_p) {
mode = mode_p;
if (mode & ~AA_VALID_NET_PERMS)
yyerror("mode contains invalid permissions for unix socket rules\n");
else if ((mode & AA_NET_BIND) &&
((mode & AA_PEER_NET_PERMS) || has_peer_conds()))
/* Do we want to loosen this? */
yyerror("unix socket 'bind' access cannot be used with message rule conditionals\n");
else if ((mode & AA_NET_LISTEN) &&
((mode & AA_PEER_NET_PERMS) || has_peer_conds()))
/* Do we want to loosen this? */
yyerror("unix socket 'listen' access cannot be used with message rule conditionals\n");
else if ((mode & AA_NET_ACCEPT) &&
((mode & AA_PEER_NET_PERMS) || has_peer_conds()))
/* Do we want to loosen this? */
yyerror("unix socket 'accept' access cannot be used with message rule conditionals\n");
} else {
mode = AA_VALID_NET_PERMS;
}
free_cond_list(conds);
free_cond_list(peer_conds);
}
ostream &unix_rule::dump_local(ostream &os)
{
af_rule::dump_local(os);
if (path)
os << "path='" << path << "'";
return os;
}
ostream &unix_rule::dump_peer(ostream &os)
{
af_rule::dump_peer(os);
if (peer_path)
os << "path='" << peer_path << "'";
return os;
}
int unix_rule::expand_variables(void)
{
int error = af_rule::expand_variables();
if (error)
return error;
error = expand_entry_variables(&path);
if (error)
return error;
error = expand_entry_variables(&peer_path);
if (error)
return error;
return 0;
}
/* do we want to warn once/profile or just once per compile?? */
static void warn_once(const char *name, const char *msg)
{
static const char *warned_name = NULL;
if (warned_name != name) {
cerr << "Warning from profile " << name << " (";
if (current_filename)
cerr << current_filename;
else
cerr << "stdin";
cerr << "): " << msg << "\n";
warned_name = name;
}
}
static void warn_once(const char *name)
{
warn_once(name, "extended network unix socket rules not enforced");
}
std::ostringstream &writeu16(std::ostringstream &o, int v)
{
u16 tmp = htobe16((u16) v);
char *c = (char *) &tmp;
o << "\\x" << std::setfill('0') << std::setw(2) << std::hex << *c++;
o << "\\x" << std::setfill('0') << std::setw(2) << std::hex << *c;
return o;
}
#define CMD_ADDR 1
#define CMD_LISTEN 2
#define CMD_ACCEPT 3
#define CMD_OPT 4
void unix_rule::downgrade_rule(Profile &prof) {
if (!prof.net.allow && !prof.alloc_net_table())
yyerror(_("Memory allocation error."));
if (deny) {
prof.net.deny[AF_UNIX] |= 1 << sock_type_n;
if (!audit)
prof.net.quiet[AF_UNIX] |= 1 << sock_type_n;
} else {
prof.net.allow[AF_UNIX] |= 1 << sock_type_n;
if (audit)
prof.net.audit[AF_UNIX] |= 1 << sock_type_n;
}
}
int unix_rule::gen_policy_re(Profile &prof)
{
std::ostringstream buffer, tmp;
std::string buf;
pattern_t ptype;
int pos;
int mask = mode;
/* always generate a downgraded rule. This doesn't change generated
* policy size and allows the binary policy to be loaded against
* older kernels and be enforced to the best of the old network
* rules ability
*/
downgrade_rule(prof);
if (!kernel_supports_unix) {
if (kernel_supports_network) {
/* only warn if we are building against a kernel
* that requires downgrading */
warn_once(prof.name, "downgrading extended network unix socket rule to generic network rule\n");
/* TODO: add ability to abort instead of downgrade */
return RULE_OK;
}
warn_once(prof.name);
return RULE_NOT_SUPPORTED;
}
buffer << "\\x" << std::setfill('0') << std::setw(2) << std::hex << AA_CLASS_NET;
buffer << writeu16(buffer, AF_UNIX);
if (sock_type)
buffer << writeu16(buffer, sock_type_n);
else
buffer << "..";
if (proto)
buffer << writeu16(buffer, proto_n);
else
buffer << "..";
if (mask & AA_NET_CREATE) {
buf = buffer.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
AA_NET_CREATE,
audit & AA_NET_CREATE,
dfaflags))
goto fail;
mask &= ~AA_NET_CREATE;
}
/* local addr */
if (path) {
if (strcmp(path, "none") == 0) {
buffer << "\\x01";
} else {
/* skip leading @ */
ptype = convert_aaregex_to_pcre(path + 1, 0, buf, &pos);
if (ptype == ePatternInvalid)
goto fail;
/* kernel starts abstract with \0 */
buffer << "\\x00";
buffer << buf;
}
} else
buffer << ".*";
/* change to out of band separator */
buffer << "\\x00";
if (mask & AA_LOCAL_NET_PERMS) {
/* local label option */
if (label) {
ptype = convert_aaregex_to_pcre(label, 0, buf, &pos);
if (ptype == ePatternInvalid)
goto fail;
/* kernel starts abstract with \0 */
buffer << buf;
} else
tmp << anyone_match_pattern;
buffer << "\\x00";
/* create already masked off */
if (mask & AA_LOCAL_NET_PERMS & ~AA_LOCAL_NET_CMD) {
buf = buffer.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
mask & AA_LOCAL_NET_PERMS & ~AA_LOCAL_NET_CMD,
audit & AA_LOCAL_NET_PERMS & ~AA_LOCAL_NET_CMD,
dfaflags))
goto fail;
}
/* cmd selector - drop accept??? */
if (mask & AA_NET_ACCEPT) {
tmp.str(buffer.str());
tmp << "\\x" << std::setfill('0') << std::setw(2) << std::hex << CMD_ACCEPT;
buf = tmp.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
AA_NET_ACCEPT,
audit & AA_NET_ACCEPT,
dfaflags))
goto fail;
}
if (mask & AA_NET_LISTEN) {
tmp.str(buffer.str());
tmp << "\\x" << std::setfill('0') << std::setw(2) << std::hex << CMD_LISTEN;
/* TODO: backlog conditional */
tmp << "..";
buf = tmp.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
AA_NET_LISTEN,
audit & AA_NET_LISTEN,
dfaflags))
goto fail;
}
if (mask & AA_NET_OPT) {
tmp.str(buffer.str());
tmp << "\\x" << std::setfill('0') << std::setw(2) << std::hex << CMD_OPT;
/* TODO: sockopt conditional */
tmp << "..";
buf = tmp.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
AA_NET_OPT,
audit & AA_NET_OPT,
dfaflags))
goto fail;
}
mask &= ~AA_LOCAL_NET_PERMS;
}
if (mask & AA_PEER_NET_PERMS) {
/* cmd selector */
buffer << "\\x" << std::setfill('0') << std::setw(2) << std::hex << CMD_ADDR;
/* peer addr */
if (peer_path) {
if (strcmp(path, "none") == 0) {
buffer << "\\x01";
} else {
/* skip leading @ */
ptype = convert_aaregex_to_pcre(peer_path + 1, 0, buf, &pos);
if (ptype == ePatternInvalid)
goto fail;
/* kernel starts abstract with \0 */
buffer << "\\x00";
buffer << buf;
}
}
/* change to out of band separator */
buffer << "\\x00";
if (peer_label) {
ptype = convert_aaregex_to_pcre(peer_label, 0, buf, &pos);
if (ptype == ePatternInvalid)
goto fail;
buffer << buf;
} else {
buffer << anyone_match_pattern;
}
buf = buffer.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny, mode & AA_PEER_NET_PERMS, audit, dfaflags))
goto fail;
}
return RULE_OK;
fail:
return RULE_ERROR;
}