diff options
Diffstat (limited to 'drivers/net/netconsole.c')
| -rw-r--r-- | drivers/net/netconsole.c | 274 |
1 files changed, 261 insertions, 13 deletions
diff --git a/drivers/net/netconsole.c b/drivers/net/netconsole.c index 57dd6821a8aa..862001d09aa8 100644 --- a/drivers/net/netconsole.c +++ b/drivers/net/netconsole.c @@ -32,8 +32,13 @@ #include <linux/moduleparam.h> #include <linux/kernel.h> #include <linux/string.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/udp.h> #include <linux/netpoll.h> #include <linux/inet.h> +#include <linux/unaligned.h> +#include <net/ip6_checksum.h> #include <linux/configfs.h> #include <linux/etherdevice.h> #include <linux/hex.h> @@ -45,6 +50,7 @@ MODULE_AUTHOR("Matt Mackall <mpm@selenic.com>"); MODULE_DESCRIPTION("Console driver for network interfaces"); MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("NETDEV_INTERNAL"); #define MAX_PARAM_LENGTH 256 #define MAX_EXTRADATA_ENTRY_LEN 256 @@ -184,8 +190,10 @@ struct netconsole_target { bool extended; bool release; struct netpoll np; - /* protected by target_list_lock */ - char buf[MAX_PRINT_CHUNK]; + /* protected by target_list_lock; +1 gives scnprintf() room for its + * NUL terminator so a full MAX_PRINT_CHUNK payload is not truncated + */ + char buf[MAX_PRINT_CHUNK + 1]; struct work_struct resume_wq; }; @@ -277,6 +285,13 @@ static bool bound_by_mac(struct netconsole_target *nt) return is_valid_ether_addr(nt->np.dev_mac); } +static void netcons_release_dev(struct netconsole_target *nt) +{ + do_netpoll_cleanup(&nt->np); + if (bound_by_mac(nt)) + memset(&nt->np.dev_name, 0, IFNAMSIZ); +} + /* Attempts to resume logging to a deactivated target. */ static void resume_target(struct netconsole_target *nt) { @@ -329,6 +344,24 @@ static void process_resume_target(struct work_struct *work) resume_target(nt); + /* netpoll_setup() took a net_device reference and dropped the RTNL + * before returning, all while this target was off target_list and + * thus invisible to netconsole_netdev_event(). If the device was + * unregistered in that window the NETDEV_UNREGISTER notifier could not + * tear this target down, which would leak the reference and hang + * unregister_netdevice(). Re-check under the RTNL before re-publishing: + * taking it across the check and the list_add() serialises against the + * notifier (which also runs under the RTNL), so the device is either + * still registered (the notifier will find the re-added target) or + * already unregistering (we drop the reference here). + */ + rtnl_lock(); + if (nt->state == STATE_ENABLED && nt->np.dev && + nt->np.dev->reg_state != NETREG_REGISTERED) { + netcons_release_dev(nt); + nt->state = STATE_DISABLED; + } + /* At this point the target is either enabled or disabled and * was cleaned up before getting deactivated. Either way, add it * back to target list. @@ -336,6 +369,7 @@ static void process_resume_target(struct work_struct *work) spin_lock_irqsave(&target_list_lock, flags); list_add(&nt->list, &target_list); spin_unlock_irqrestore(&target_list_lock, flags); + rtnl_unlock(); out_unlock: dynamic_netconsole_mutex_unlock(); @@ -383,9 +417,7 @@ static void netconsole_process_cleanups_core(void) list_for_each_entry_safe(nt, tmp, &target_cleanup_list, list) { /* all entries in the cleanup_list needs to be disabled */ WARN_ON_ONCE(nt->state == STATE_ENABLED); - do_netpoll_cleanup(&nt->np); - if (bound_by_mac(nt)) - memset(&nt->np.dev_name, 0, IFNAMSIZ); + netcons_release_dev(nt); /* moved the cleaned target to target_list. Need to hold both * locks */ @@ -1258,7 +1290,7 @@ static void userdatum_release(struct config_item *item) kfree(to_userdatum(item)); } -static struct configfs_item_operations userdatum_ops = { +static const struct configfs_item_operations userdatum_ops = { .release = userdatum_release, }; @@ -1313,7 +1345,7 @@ static struct configfs_attribute *userdata_attrs[] = { NULL, }; -static struct configfs_group_operations userdata_ops = { +static const struct configfs_group_operations userdata_ops = { .make_item = userdatum_make_item, .drop_item = userdatum_drop, }; @@ -1364,7 +1396,7 @@ static void netconsole_target_release(struct config_item *item) kfree(nt); } -static struct configfs_item_operations netconsole_target_item_ops = { +static const struct configfs_item_operations netconsole_target_item_ops = { .release = netconsole_target_release, }; @@ -1443,10 +1475,21 @@ static void drop_netconsole_target(struct config_group *group, { struct netconsole_target *nt = to_target(item); unsigned long flags; + bool needs_cleanup; dynamic_netconsole_mutex_lock(); + mutex_lock(&target_cleanup_list_lock); spin_lock_irqsave(&target_list_lock, flags); + /* A STATE_DEACTIVATED target may have been moved to + * target_cleanup_list by netconsole_netdev_event() but not yet + * processed by netconsole_process_cleanups_core(). Unlinking it below + * hides it from the cleanup worker, so this path has to clean it up + * itself. Record that the target still owns a netpoll before the + * state is downgraded. + */ + needs_cleanup = nt->state == STATE_ENABLED || + nt->state == STATE_DEACTIVATED; /* Disable deactivated target to prevent races between resume attempt * and target removal. */ @@ -1454,6 +1497,7 @@ static void drop_netconsole_target(struct config_group *group, nt->state = STATE_DISABLED; list_del(&nt->list); spin_unlock_irqrestore(&target_list_lock, flags); + mutex_unlock(&target_cleanup_list_lock); dynamic_netconsole_mutex_unlock(); @@ -1467,14 +1511,16 @@ static void drop_netconsole_target(struct config_group *group, /* * The target may have never been enabled, or was manually disabled * before being removed so netpoll may have already been cleaned up. + * netpoll_cleanup() is idempotent (it skips when np->dev is NULL), so + * it is safe even if the cleanup worker already tore the netpoll down. */ - if (nt->state == STATE_ENABLED) + if (needs_cleanup) netpoll_cleanup(&nt->np); config_item_put(&nt->group.cg_item); } -static struct configfs_group_operations netconsole_subsys_group_ops = { +static const struct configfs_group_operations netconsole_subsys_group_ops = { .make_group = make_netconsole_target, .drop_item = drop_netconsole_target, }; @@ -1648,6 +1694,208 @@ static struct notifier_block netconsole_netdev_notifier = { .notifier_call = netconsole_netdev_event, }; +/* Pop a pre-allocated skb from the pool and request a refill. + * + * The pool is refilled with MAX_SKB_SIZE buffers, so a pooled skb cannot + * satisfy a larger request. Return NULL in that case rather than handing + * back a too-small skb that would later trip skb_over_panic() in skb_put(); + * the caller still polls and retries, and alloc_skb() itself can satisfy the + * oversized request once memory frees up. + * + * The refill is requested via schedule_work(), which takes the workqueue + * pool locks and is therefore not NMI-safe. Skip the refill when called + * from NMI context; the next non-NMI caller will top the pool back up. + */ +static struct sk_buff *netcons_skb_pop(struct netpoll *np, int len) +{ + struct sk_buff *skb; + + if (len > MAX_SKB_SIZE) { + /* net_warn_ratelimited() pulls in printk machinery that is not + * NMI-safe and could recurse into the nbcon console we are + * servicing, so only warn outside NMI. + */ + if (!in_nmi()) + net_warn_ratelimited("netconsole: dropping message, requested skb len %d exceeds pool buffer size %zu on %s\n", + len, (size_t)MAX_SKB_SIZE, + np->dev->name); + return NULL; + } + + skb = skb_dequeue(&np->skb_pool); + if (!in_nmi()) + schedule_work(&np->refill_wq); + + return skb; +} + +static struct sk_buff *find_skb(struct netpoll *np, int len, int reserve) +{ + int count = 0; + struct sk_buff *skb; + + netpoll_zap_completion_queue(); +repeat: + + skb = alloc_skb(len, GFP_ATOMIC); + if (!skb) + skb = netcons_skb_pop(np, len); + + if (!skb) { + if (++count < 10) { + netpoll_poll_dev(np->dev); + goto repeat; + } + return NULL; + } + + refcount_set(&skb->users, 1); + skb_reserve(skb, reserve); + return skb; +} + +static void netpoll_udp_checksum(struct netpoll *np, struct sk_buff *skb, + int len) +{ + struct udphdr *udph; + int udp_len; + + udp_len = len + sizeof(struct udphdr); + udph = udp_hdr(skb); + + /* check needs to be set, since it will be consumed in csum_partial */ + udph->check = 0; + if (np->ipv6) + udph->check = csum_ipv6_magic(&np->local_ip.in6, + &np->remote_ip.in6, + udp_len, IPPROTO_UDP, + csum_partial(udph, udp_len, 0)); + else + udph->check = csum_tcpudp_magic(np->local_ip.ip, + np->remote_ip.ip, + udp_len, IPPROTO_UDP, + csum_partial(udph, udp_len, 0)); + if (udph->check == 0) + udph->check = CSUM_MANGLED_0; +} + +static void push_udp(struct netpoll *np, struct sk_buff *skb, int len) +{ + struct udphdr *udph; + int udp_len; + + udp_len = len + sizeof(struct udphdr); + + skb_push(skb, sizeof(struct udphdr)); + skb_reset_transport_header(skb); + + udph = udp_hdr(skb); + udph->source = htons(np->local_port); + udph->dest = htons(np->remote_port); + udph->len = htons(udp_len); + + netpoll_udp_checksum(np, skb, len); +} + +static void push_eth(struct netpoll *np, struct sk_buff *skb) +{ + struct ethhdr *eth; + + eth = skb_push(skb, ETH_HLEN); + skb_reset_mac_header(skb); + ether_addr_copy(eth->h_source, np->dev->dev_addr); + ether_addr_copy(eth->h_dest, np->remote_mac); + if (np->ipv6) + eth->h_proto = htons(ETH_P_IPV6); + else + eth->h_proto = htons(ETH_P_IP); +} + +static void push_ipv4(struct netpoll *np, struct sk_buff *skb, int len) +{ + static atomic_t ip_ident; + struct iphdr *iph; + int ip_len; + + ip_len = len + sizeof(struct udphdr) + sizeof(struct iphdr); + + skb_push(skb, sizeof(struct iphdr)); + skb_reset_network_header(skb); + iph = ip_hdr(skb); + + /* iph->version = 4; iph->ihl = 5; */ + *(unsigned char *)iph = 0x45; + iph->tos = 0; + put_unaligned(htons(ip_len), &iph->tot_len); + iph->id = htons(atomic_inc_return(&ip_ident)); + iph->frag_off = 0; + iph->ttl = 64; + iph->protocol = IPPROTO_UDP; + iph->check = 0; + put_unaligned(np->local_ip.ip, &iph->saddr); + put_unaligned(np->remote_ip.ip, &iph->daddr); + iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl); + skb->protocol = htons(ETH_P_IP); +} + +static void push_ipv6(struct netpoll *np, struct sk_buff *skb, int len) +{ + struct ipv6hdr *ip6h; + + skb_push(skb, sizeof(struct ipv6hdr)); + skb_reset_network_header(skb); + ip6h = ipv6_hdr(skb); + + /* ip6h->version = 6; ip6h->priority = 0; */ + *(unsigned char *)ip6h = 0x60; + ip6h->flow_lbl[0] = 0; + ip6h->flow_lbl[1] = 0; + ip6h->flow_lbl[2] = 0; + + ip6h->payload_len = htons(sizeof(struct udphdr) + len); + ip6h->nexthdr = IPPROTO_UDP; + ip6h->hop_limit = 32; + ip6h->saddr = np->local_ip.in6; + ip6h->daddr = np->remote_ip.in6; + + skb->protocol = htons(ETH_P_IPV6); +} + +static int netpoll_send_udp(struct netpoll *np, const char *msg, int len) +{ + int total_len, ip_len, udp_len; + struct sk_buff *skb; + + if (!IS_ENABLED(CONFIG_PREEMPT_RT)) + WARN_ON_ONCE(!irqs_disabled()); + + udp_len = len + sizeof(struct udphdr); + if (np->ipv6) + ip_len = udp_len + sizeof(struct ipv6hdr); + else + ip_len = udp_len + sizeof(struct iphdr); + + total_len = ip_len + LL_RESERVED_SPACE(np->dev); + + skb = find_skb(np, total_len + np->dev->needed_tailroom, + total_len - len); + if (!skb) + return -ENOMEM; + + skb_copy_to_linear_data(skb, msg, len); + skb_put(skb, len); + + push_udp(np, skb, len); + if (np->ipv6) + push_ipv6(np, skb, len); + else + push_ipv4(np, skb, len); + push_eth(np, skb); + skb->dev = np->dev; + + return (int)netpoll_send_skb(np, skb); +} + /** * send_udp - Wrapper for netpoll_send_udp that counts errors * @nt: target to send message to @@ -1692,7 +1940,7 @@ static void send_msg_no_fragmentation(struct netconsole_target *nt, if (release_len) { release = init_utsname()->release; - scnprintf(nt->buf, MAX_PRINT_CHUNK, "%s,%.*s", release, + scnprintf(nt->buf, sizeof(nt->buf), "%s,%.*s", release, msg_len, msg); msg_len += release_len; } else { @@ -1701,12 +1949,12 @@ static void send_msg_no_fragmentation(struct netconsole_target *nt, if (userdata) msg_len += scnprintf(&nt->buf[msg_len], - MAX_PRINT_CHUNK - msg_len, "%s", + sizeof(nt->buf) - msg_len, "%s", userdata); if (sysdata) msg_len += scnprintf(&nt->buf[msg_len], - MAX_PRINT_CHUNK - msg_len, "%s", + sizeof(nt->buf) - msg_len, "%s", sysdata); send_udp(nt, nt->buf, msg_len); |
