apparmor/parser/af_unix.cc
John Johansen 855dbd4ac8 parser: fix rule downgrade for unix rules
Rule downgrades are used to provide some confinement when a feature
is only partially supported by the kernel.

  Eg. On a kernel that doesn't support fine grained af_unix mediation
      but does support network mediation.

        unix (connect, receive, send)
              type=stream
              peer=(addr="@/tmp/.ICE-unix/[0-9]*"),

      will be downgraded to

        network unix type=stream,

Which while more permissive still provides some mediation while
allowing the appication to still function. However making the rule
a deny rule result in tightening the profile.

  Eg.
        deny unix (connect, receive, send)
              type=stream
              peer=(addr="@/tmp/.ICE-unix/[0-9]*"),

      will be downgraded to

        deny network unix type=stream,

and that deny rule will take priority over any allow rule. Which means
that if the profile also had unix allow rules they will get blocked by
the downgraded deny rule, because deny rules have a higher priority,
and the application will break. Even worse there is no way to add the
functionality back to the profile without deleting the offending deny
rule.

To fix this we drop deny rules that can't be downgraded in a way that
won't break the application.

Fixes: https://bugzilla.opensuse.org/show_bug.cgi?id=1180766
MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/700
Signed-off-by: John Johansen <john.johansen@canonical.com>
2021-03-14 07:16:24 -07:00

444 lines
12 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 <sstream>
#include "network.h"
#include "parser.h"
#include "profile.h"
#include "af_unix.h"
/* See unix(7) for autobind address definition */
#define autobind_address_pattern "\\x00[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]";
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[] = {
{ "addr", true, false, false, either_cond },
{ NULL, false, false, false, local_cond }, /* sentinel */
};
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, "addr") == 0) {
move_conditional_value("unix socket", &addr, ent);
if (addr[0] != '@' &&
!(strcmp(addr, "none") == 0 ||
strcmp(addr, "auto") == 0))
yyerror("unix rule: invalid value for addr='%s'\n", addr);
}
/* 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, "addr") == 0) {
move_conditional_value("unix", &peer_addr, ent);
if ((peer_addr[0] != '@') &&
!(strcmp(peer_addr, "none") == 0 ||
strcmp(peer_addr, "auto") == 0))
yyerror("unix rule: invalid value for addr='%s'\n", peer_addr);
}
}
}
unix_rule::unix_rule(unsigned int type_p, bool audit_p, bool denied):
af_rule("unix"), addr(NULL), peer_addr(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"), addr(NULL), peer_addr(NULL),
audit(0), deny(0)
{
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_PEER_NET_PERMS) && has_peer_conds())
yyerror("unix socket 'create', 'shutdown', 'setattr', 'getattr', 'bind', 'listen', 'setopt', and/or 'getopt' accesses cannot be used with peer socket 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 (addr)
os << "addr='" << addr << "'";
return os;
}
ostream &unix_rule::dump_peer(ostream &os)
{
af_rule::dump_peer(os);
if (peer_addr)
os << "addr='" << peer_addr << "'";
return os;
}
int unix_rule::expand_variables(void)
{
int error = af_rule::expand_variables();
if (error)
return error;
error = expand_entry_variables(&addr);
if (error)
return error;
filter_slashes(addr);
error = expand_entry_variables(&peer_addr);
if (error)
return error;
filter_slashes(peer_addr);
return 0;
}
void unix_rule::warn_once(const char *name)
{
rule_t::warn_once(name, "extended network unix socket rules not enforced");
}
static void writeu16(std::ostringstream &o, int v)
{
u16 tmp = htobe16((u16) v);
u8 *byte1 = (u8 *)&tmp;
u8 *byte2 = byte1 + 1;
o << "\\x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<unsigned int>(*byte1);
o << "\\x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<unsigned int>(*byte2);
}
#define CMD_ADDR 1
#define CMD_LISTEN 2
#define CMD_ACCEPT 3
#define CMD_OPT 4
void unix_rule::downgrade_rule(Profile &prof) {
unsigned int mask = (unsigned int) -1;
if (!prof.net.allow && !prof.alloc_net_table())
yyerror(_("Memory allocation error."));
if (sock_type_n != -1)
mask = 1 << sock_type_n;
if (!deny) {
prof.net.allow[AF_UNIX] |= mask;
if (audit)
prof.net.audit[AF_UNIX] |= mask;
} else {
/* deny rules have to be dropped because the downgrade makes
* the rule less specific meaning it will make the profile more
* restrictive and may end up denying accesses that might be
* allowed by the profile.
*/
if (warnflags & WARN_RULE_NOT_ENFORCED)
rule_t::warn_once(prof.name, "deny unix socket rule not enforced, can't be downgraded to generic network rule\n");
}
}
void unix_rule::write_to_prot(std::ostringstream &buffer)
{
int c = features_supports_networkv8 ? AA_CLASS_NETV8 : AA_CLASS_NET;
buffer << "\\x" << std::setfill('0') << std::setw(2) << std::hex << c;
writeu16(buffer, AF_UNIX);
if (sock_type)
writeu16(buffer, sock_type_n);
else
buffer << "..";
if (proto)
writeu16(buffer, proto_n);
else
buffer << "..";
}
bool unix_rule::write_addr(std::ostringstream &buffer, const char *addr)
{
std::string buf;
pattern_t ptype;
if (addr) {
int pos;
if (strcmp(addr, "none") == 0) {
/* anonymous */
buffer << "\\x01";
} else if (strcmp(addr, "auto") == 0) {
/* autobind - special autobind rule written already
* just generate pattern that matches autobind
* generated addresses.
*/
buffer << autobind_address_pattern;
} else {
/* skip leading @ */
ptype = convert_aaregex_to_pcre(addr + 1, 0, glob_null, buf, &pos);
if (ptype == ePatternInvalid)
return false;
/* kernel starts abstract with \0 */
buffer << "\\x00";
buffer << buf;
}
} else
/* match any addr or anonymous */
buffer << ".*";
/* todo: change to out of band separator */
buffer << "\\x00";
return true;
}
bool unix_rule::write_label(std::ostringstream &buffer, const char *label)
{
std::string buf;
pattern_t ptype;
if (label) {
int pos;
ptype = convert_aaregex_to_pcre(label, 0, glob_default, buf, &pos);
if (ptype == ePatternInvalid)
return false;
/* kernel starts abstract with \0 */
buffer << buf;
} else
buffer << default_match_pattern;
return true;
}
/* General Layout
*
* Local socket end point perms
* CLASS_NET AF TYPE PROTO local (addr\0label) \0 cmd cmd_option
* ^ ^ ^ ^ ^
* | | | | |
* stub perm | | | |
* | | | |
* sub stub perm | | |
* | | |
* create perm | |
* | |
* | |
* bind, accept, get/set attr |
* |
* listen, set/get opt perm
*
*
* peer socket end point perms
* CLASS_NET AF TYPE PROTO local(addr\0label\0) cmd_addr peer(addr\0label )
* ^
* |
* send/receive connect/accept perm
*
* NOTE: accept is encoded twice, locally to check if a socket is allowed
* to accept, and then as a pair to test that it can accept the pair.
*/
int unix_rule::gen_policy_re(Profile &prof)
{
std::ostringstream buffer;
std::string buf;
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 (!features_supports_unix) {
if (features_supports_network || features_supports_networkv8) {
/* only warn if we are building against a kernel
* that requires downgrading */
if (warnflags & WARN_RULE_DOWNGRADED)
rule_t::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;
} else {
warn_once(prof.name);
}
return RULE_NOT_SUPPORTED;
}
write_to_prot(buffer);
if ((mask & AA_NET_CREATE) && !has_peer_conds()) {
buf = buffer.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
map_perms(AA_NET_CREATE),
map_perms(audit & AA_NET_CREATE),
dfaflags))
goto fail;
mask &= ~AA_NET_CREATE;
}
/* write special pattern for autobind? Will not grant bind
* on any specific address
*/
if ((mask & AA_NET_BIND) && (!addr || (strcmp(addr, "auto") == 0))) {
std::ostringstream tmp;
tmp << buffer.str();
/* todo: change to out of band separator */
/* skip addr, its 0 length */
tmp << "\\x00";
/* local label option */
if (!write_label(tmp, label))
goto fail;
/* separator */
tmp << "\\x00";
buf = tmp.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
map_perms(AA_NET_BIND),
map_perms(audit & AA_NET_BIND),
dfaflags))
goto fail;
/* clear if auto, else generic need to generate addr below */
if (addr)
mask &= ~AA_NET_BIND;
}
if (mask) {
/* local addr */
if (!write_addr(buffer, addr))
goto fail;
/* local label option */
if (!write_label(buffer, label))
goto fail;
/* separator */
buffer << "\\x00";
/* create already masked off */
int local_mask = has_peer_conds() ? AA_NET_ACCEPT :
AA_LOCAL_NET_PERMS & ~AA_LOCAL_NET_CMD;
if (mask & local_mask) {
buf = buffer.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
map_perms(mask & local_mask),
map_perms(audit & local_mask),
dfaflags))
goto fail;
}
if ((mask & AA_NET_LISTEN) && !has_peer_conds()) {
std::ostringstream tmp(buffer.str());
tmp.seekp(0, ios_base::end);
tmp << "\\x" << std::setfill('0') << std::setw(2) << std::hex << CMD_LISTEN;
/* TODO: backlog conditional: for now match anything*/
tmp << "..";
buf = tmp.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
map_perms(AA_NET_LISTEN),
map_perms(audit & AA_NET_LISTEN),
dfaflags))
goto fail;
}
if ((mask & AA_NET_OPT) && !has_peer_conds()) {
std::ostringstream tmp(buffer.str());
tmp.seekp(0, ios_base::end);
tmp << "\\x" << std::setfill('0') << std::setw(2) << std::hex << CMD_OPT;
/* TODO: sockopt conditional: for now match anything */
tmp << "..";
buf = tmp.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny,
map_perms(mask & AA_NET_OPT),
map_perms(audit & AA_NET_OPT),
dfaflags))
goto fail;
}
mask &= ~AA_LOCAL_NET_PERMS | AA_NET_ACCEPT;
} /* if (mask) */
if (mask & AA_PEER_NET_PERMS) {
/* cmd selector */
buffer << "\\x" << std::setfill('0') << std::setw(2) << std::hex << CMD_ADDR;
/* local addr */
if (!write_addr(buffer, peer_addr))
goto fail;
/* local label option */
if (!write_label(buffer, peer_label))
goto fail;
buf = buffer.str();
if (!prof.policy.rules->add_rule(buf.c_str(), deny, map_perms(mode & AA_PEER_NET_PERMS), map_perms(audit), dfaflags))
goto fail;
}
return RULE_OK;
fail:
return RULE_ERROR;
}