summaryrefslogtreecommitdiff
path: root/net/netlink/policy.c
diff options
context:
space:
mode:
authorJohannes Berg <johannes.berg@intel.com>2020-04-30 22:13:12 +0200
committerDavid S. Miller <davem@davemloft.net>2020-04-30 17:51:42 -0700
commitd07dcf9aadd6b2842b439e8668ff7ea2873f28d7 (patch)
tree65bead82c5e5ff278acbe657226f91c93a58019f /net/netlink/policy.c
parent2c28ae48f24d84fcda31fb8acaf2edca6ec46c49 (diff)
netlink: add infrastructure to expose policies to userspace
Add, and use in generic netlink, helpers to dump out a netlink policy to userspace, including all the range validation data, nested policies etc. This lets userspace discover what the kernel understands. For families/commands other than generic netlink, the helpers need to be used directly in an appropriate command, or we can add some infrastructure (a new netlink family) that those can register their policies with for introspection. I'm not that familiar with non-generic netlink, so that's left out for now. The data exposed to userspace also includes min and max length for binary/string data, I've done that instead of letting the userspace tools figure out whether min/max is intended based on the type so that we can extend this later in the kernel, we might want to just use the range data for example. Because of this, I opted to not directly expose the NLA_* values, even if some of them are already exposed via BPF, as with min/max length we don't need to have different types here for NLA_BINARY/NLA_MIN_LEN/NLA_EXACT_LEN, we just make them all NL_ATTR_TYPE_BINARY with min/max length optionally set. Similarly, we don't really need NLA_MSECS, and perhaps can remove it in the future - but not if we encode it into the userspace API now. It gets mapped to NL_ATTR_TYPE_U64 here. Note that the exposing here corresponds to the strict policy interpretation, and NLA_UNSPEC items are omitted entirely. To get those, change them to NLA_MIN_LEN which behaves in exactly the same way, but is exposed. Signed-off-by: Johannes Berg <johannes.berg@intel.com> Signed-off-by: David S. Miller <davem@davemloft.net>
Diffstat (limited to 'net/netlink/policy.c')
-rw-r--r--net/netlink/policy.c308
1 files changed, 308 insertions, 0 deletions
diff --git a/net/netlink/policy.c b/net/netlink/policy.c
new file mode 100644
index 000000000000..f6491853c797
--- /dev/null
+++ b/net/netlink/policy.c
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * NETLINK Policy advertisement to userspace
+ *
+ * Authors: Johannes Berg <johannes@sipsolutions.net>
+ *
+ * Copyright 2019 Intel Corporation
+ */
+
+#include <linux/kernel.h>
+#include <linux/errno.h>
+#include <linux/types.h>
+#include <net/netlink.h>
+
+#define INITIAL_POLICIES_ALLOC 10
+
+struct nl_policy_dump {
+ unsigned int policy_idx;
+ unsigned int attr_idx;
+ unsigned int n_alloc;
+ struct {
+ const struct nla_policy *policy;
+ unsigned int maxtype;
+ } policies[];
+};
+
+static int add_policy(struct nl_policy_dump **statep,
+ const struct nla_policy *policy,
+ unsigned int maxtype)
+{
+ struct nl_policy_dump *state = *statep;
+ unsigned int n_alloc, i;
+
+ if (!policy || !maxtype)
+ return 0;
+
+ for (i = 0; i < state->n_alloc; i++) {
+ if (state->policies[i].policy == policy)
+ return 0;
+
+ if (!state->policies[i].policy) {
+ state->policies[i].policy = policy;
+ state->policies[i].maxtype = maxtype;
+ return 0;
+ }
+ }
+
+ n_alloc = state->n_alloc + INITIAL_POLICIES_ALLOC;
+ state = krealloc(state, struct_size(state, policies, n_alloc),
+ GFP_KERNEL);
+ if (!state)
+ return -ENOMEM;
+
+ state->policies[state->n_alloc].policy = policy;
+ state->policies[state->n_alloc].maxtype = maxtype;
+ state->n_alloc = n_alloc;
+ *statep = state;
+
+ return 0;
+}
+
+static unsigned int get_policy_idx(struct nl_policy_dump *state,
+ const struct nla_policy *policy)
+{
+ unsigned int i;
+
+ for (i = 0; i < state->n_alloc; i++) {
+ if (state->policies[i].policy == policy)
+ return i;
+ }
+
+ WARN_ON_ONCE(1);
+ return -1;
+}
+
+int netlink_policy_dump_start(const struct nla_policy *policy,
+ unsigned int maxtype,
+ unsigned long *_state)
+{
+ struct nl_policy_dump *state;
+ unsigned int policy_idx;
+ int err;
+
+ /* also returns 0 if "*_state" is our ERR_PTR() end marker */
+ if (*_state)
+ return 0;
+
+ /*
+ * walk the policies and nested ones first, and build
+ * a linear list of them.
+ */
+
+ state = kzalloc(struct_size(state, policies, INITIAL_POLICIES_ALLOC),
+ GFP_KERNEL);
+ if (!state)
+ return -ENOMEM;
+ state->n_alloc = INITIAL_POLICIES_ALLOC;
+
+ err = add_policy(&state, policy, maxtype);
+ if (err)
+ return err;
+
+ for (policy_idx = 0;
+ policy_idx < state->n_alloc && state->policies[policy_idx].policy;
+ policy_idx++) {
+ const struct nla_policy *policy;
+ unsigned int type;
+
+ policy = state->policies[policy_idx].policy;
+
+ for (type = 0;
+ type <= state->policies[policy_idx].maxtype;
+ type++) {
+ switch (policy[type].type) {
+ case NLA_NESTED:
+ case NLA_NESTED_ARRAY:
+ err = add_policy(&state,
+ policy[type].nested_policy,
+ policy[type].len);
+ if (err)
+ return err;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ *_state = (unsigned long)state;
+
+ return 0;
+}
+
+static bool netlink_policy_dump_finished(struct nl_policy_dump *state)
+{
+ return state->policy_idx >= state->n_alloc ||
+ !state->policies[state->policy_idx].policy;
+}
+
+bool netlink_policy_dump_loop(unsigned long *_state)
+{
+ struct nl_policy_dump *state = (void *)*_state;
+
+ if (IS_ERR(state))
+ return false;
+
+ if (netlink_policy_dump_finished(state)) {
+ kfree(state);
+ /* store end marker instead of freed state */
+ *_state = (unsigned long)ERR_PTR(-ENOENT);
+ return false;
+ }
+
+ return true;
+}
+
+int netlink_policy_dump_write(struct sk_buff *skb, unsigned long _state)
+{
+ struct nl_policy_dump *state = (void *)_state;
+ const struct nla_policy *pt;
+ struct nlattr *policy, *attr;
+ enum netlink_attribute_type type;
+ bool again;
+
+send_attribute:
+ again = false;
+
+ pt = &state->policies[state->policy_idx].policy[state->attr_idx];
+
+ policy = nla_nest_start(skb, state->policy_idx);
+ if (!policy)
+ return -ENOBUFS;
+
+ attr = nla_nest_start(skb, state->attr_idx);
+ if (!attr)
+ goto nla_put_failure;
+
+ switch (pt->type) {
+ default:
+ case NLA_UNSPEC:
+ case NLA_REJECT:
+ /* skip - use NLA_MIN_LEN to advertise such */
+ nla_nest_cancel(skb, policy);
+ again = true;
+ goto next;
+ case NLA_NESTED:
+ type = NL_ATTR_TYPE_NESTED;
+ /* fall through */
+ case NLA_NESTED_ARRAY:
+ if (pt->type == NLA_NESTED_ARRAY)
+ type = NL_ATTR_TYPE_NESTED_ARRAY;
+ if (pt->nested_policy && pt->len &&
+ (nla_put_u32(skb, NL_POLICY_TYPE_ATTR_POLICY_IDX,
+ get_policy_idx(state, pt->nested_policy)) ||
+ nla_put_u32(skb, NL_POLICY_TYPE_ATTR_POLICY_MAXTYPE,
+ pt->len)))
+ goto nla_put_failure;
+ break;
+ case NLA_U8:
+ case NLA_U16:
+ case NLA_U32:
+ case NLA_U64:
+ case NLA_MSECS: {
+ struct netlink_range_validation range;
+
+ if (pt->type == NLA_U8)
+ type = NL_ATTR_TYPE_U8;
+ else if (pt->type == NLA_U16)
+ type = NL_ATTR_TYPE_U16;
+ else if (pt->type == NLA_U32)
+ type = NL_ATTR_TYPE_U32;
+ else
+ type = NL_ATTR_TYPE_U64;
+
+ nla_get_range_unsigned(pt, &range);
+
+ if (nla_put_u64_64bit(skb, NL_POLICY_TYPE_ATTR_MIN_VALUE_U,
+ range.min, NL_POLICY_TYPE_ATTR_PAD) ||
+ nla_put_u64_64bit(skb, NL_POLICY_TYPE_ATTR_MAX_VALUE_U,
+ range.max, NL_POLICY_TYPE_ATTR_PAD))
+ goto nla_put_failure;
+ break;
+ }
+ case NLA_S8:
+ case NLA_S16:
+ case NLA_S32:
+ case NLA_S64: {
+ struct netlink_range_validation_signed range;
+
+ if (pt->type == NLA_S8)
+ type = NL_ATTR_TYPE_S8;
+ else if (pt->type == NLA_S16)
+ type = NL_ATTR_TYPE_S16;
+ else if (pt->type == NLA_S32)
+ type = NL_ATTR_TYPE_S32;
+ else
+ type = NL_ATTR_TYPE_S64;
+
+ nla_get_range_signed(pt, &range);
+
+ if (nla_put_s64(skb, NL_POLICY_TYPE_ATTR_MIN_VALUE_S,
+ range.min, NL_POLICY_TYPE_ATTR_PAD) ||
+ nla_put_s64(skb, NL_POLICY_TYPE_ATTR_MAX_VALUE_S,
+ range.max, NL_POLICY_TYPE_ATTR_PAD))
+ goto nla_put_failure;
+ break;
+ }
+ case NLA_BITFIELD32:
+ type = NL_ATTR_TYPE_BITFIELD32;
+ if (nla_put_u32(skb, NL_POLICY_TYPE_ATTR_BITFIELD32_MASK,
+ pt->bitfield32_valid))
+ goto nla_put_failure;
+ break;
+ case NLA_EXACT_LEN:
+ type = NL_ATTR_TYPE_BINARY;
+ if (nla_put_u32(skb, NL_POLICY_TYPE_ATTR_MIN_LENGTH, pt->len) ||
+ nla_put_u32(skb, NL_POLICY_TYPE_ATTR_MAX_LENGTH, pt->len))
+ goto nla_put_failure;
+ break;
+ case NLA_STRING:
+ case NLA_NUL_STRING:
+ case NLA_BINARY:
+ if (pt->type == NLA_STRING)
+ type = NL_ATTR_TYPE_STRING;
+ else if (pt->type == NLA_NUL_STRING)
+ type = NL_ATTR_TYPE_NUL_STRING;
+ else
+ type = NL_ATTR_TYPE_BINARY;
+ if (pt->len && nla_put_u32(skb, NL_POLICY_TYPE_ATTR_MAX_LENGTH,
+ pt->len))
+ goto nla_put_failure;
+ break;
+ case NLA_MIN_LEN:
+ type = NL_ATTR_TYPE_BINARY;
+ if (nla_put_u32(skb, NL_POLICY_TYPE_ATTR_MIN_LENGTH, pt->len))
+ goto nla_put_failure;
+ break;
+ case NLA_FLAG:
+ type = NL_ATTR_TYPE_FLAG;
+ break;
+ }
+
+ if (nla_put_u32(skb, NL_POLICY_TYPE_ATTR_TYPE, type))
+ goto nla_put_failure;
+
+ /* finish and move state to next attribute */
+ nla_nest_end(skb, attr);
+ nla_nest_end(skb, policy);
+
+next:
+ state->attr_idx += 1;
+ if (state->attr_idx > state->policies[state->policy_idx].maxtype) {
+ state->attr_idx = 0;
+ state->policy_idx++;
+ }
+
+ if (again) {
+ if (netlink_policy_dump_finished(state))
+ return -ENODATA;
+ goto send_attribute;
+ }
+
+ return 0;
+
+nla_put_failure:
+ nla_nest_cancel(skb, policy);
+ return -ENOBUFS;
+}