Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

net: fix wrong skb_get() usage / crash in IGMP/MLD parsing code

The recent refactoring of the IGMP and MLD parsing code into
ipv6_mc_check_mld() / ip_mc_check_igmp() introduced a potential crash /
BUG() invocation for bridges:

I wrongly assumed that skb_get() could be used as a simple reference
counter for an skb which is not the case. skb_get() bears additional
semantics, a user count. This leads to a BUG() invocation in
pskb_expand_head() / kernel panic if pskb_may_pull() is called on an skb
with a user count greater than one - unfortunately the refactoring did
just that.

Fixing this by removing the skb_get() call and changing the API: The
caller of ipv6_mc_check_mld() / ip_mc_check_igmp() now needs to
additionally check whether the returned skb_trimmed is a clone.

Fixes: 9afd85c9e455 ("net: Export IGMP/MLD message validation code")
Reported-by: Brenden Blanco <bblanco@plumgrid.com>
Signed-off-by: Linus Lüssing <linus.luessing@c0d3.blue>
Acked-by: Alexei Starovoitov <ast@plumgrid.com>
Signed-off-by: David S. Miller <davem@davemloft.net>

authored by

Linus Lüssing and committed by
David S. Miller
a516993f 5b3e2e14

+56 -51
+2 -2
net/bridge/br_multicast.c
··· 1591 1591 break; 1592 1592 } 1593 1593 1594 - if (skb_trimmed) 1594 + if (skb_trimmed && skb_trimmed != skb) 1595 1595 kfree_skb(skb_trimmed); 1596 1596 1597 1597 return err; ··· 1636 1636 break; 1637 1637 } 1638 1638 1639 - if (skb_trimmed) 1639 + if (skb_trimmed && skb_trimmed != skb) 1640 1640 kfree_skb(skb_trimmed); 1641 1641 1642 1642 return err;
+18 -19
net/core/skbuff.c
··· 4022 4022 * Otherwise returns the provided skb. Returns NULL in error cases 4023 4023 * (e.g. transport_len exceeds skb length or out-of-memory). 4024 4024 * 4025 - * Caller needs to set the skb transport header and release the returned skb. 4026 - * Provided skb is consumed. 4025 + * Caller needs to set the skb transport header and free any returned skb if it 4026 + * differs from the provided skb. 4027 4027 */ 4028 4028 static struct sk_buff *skb_checksum_maybe_trim(struct sk_buff *skb, 4029 4029 unsigned int transport_len) ··· 4032 4032 unsigned int len = skb_transport_offset(skb) + transport_len; 4033 4033 int ret; 4034 4034 4035 - if (skb->len < len) { 4036 - kfree_skb(skb); 4035 + if (skb->len < len) 4037 4036 return NULL; 4038 - } else if (skb->len == len) { 4037 + else if (skb->len == len) 4039 4038 return skb; 4040 - } 4041 4039 4042 4040 skb_chk = skb_clone(skb, GFP_ATOMIC); 4043 - kfree_skb(skb); 4044 - 4045 4041 if (!skb_chk) 4046 4042 return NULL; 4047 4043 ··· 4062 4066 * If the skb has data beyond the given transport length, then a 4063 4067 * trimmed & cloned skb is checked and returned. 4064 4068 * 4065 - * Caller needs to set the skb transport header and release the returned skb. 4066 - * Provided skb is consumed. 4069 + * Caller needs to set the skb transport header and free any returned skb if it 4070 + * differs from the provided skb. 4067 4071 */ 4068 4072 struct sk_buff *skb_checksum_trimmed(struct sk_buff *skb, 4069 4073 unsigned int transport_len, ··· 4075 4079 4076 4080 skb_chk = skb_checksum_maybe_trim(skb, transport_len); 4077 4081 if (!skb_chk) 4078 - return NULL; 4082 + goto err; 4079 4083 4080 - if (!pskb_may_pull(skb_chk, offset)) { 4081 - kfree_skb(skb_chk); 4082 - return NULL; 4083 - } 4084 + if (!pskb_may_pull(skb_chk, offset)) 4085 + goto err; 4084 4086 4085 4087 __skb_pull(skb_chk, offset); 4086 4088 ret = skb_chkf(skb_chk); 4087 4089 __skb_push(skb_chk, offset); 4088 4090 4089 - if (ret) { 4090 - kfree_skb(skb_chk); 4091 - return NULL; 4092 - } 4091 + if (ret) 4092 + goto err; 4093 4093 4094 4094 return skb_chk; 4095 + 4096 + err: 4097 + if (skb_chk && skb_chk != skb) 4098 + kfree_skb(skb_chk); 4099 + 4100 + return NULL; 4101 + 4095 4102 } 4096 4103 EXPORT_SYMBOL(skb_checksum_trimmed); 4097 4104
+18 -15
net/ipv4/igmp.c
··· 1435 1435 struct sk_buff *skb_chk; 1436 1436 unsigned int transport_len; 1437 1437 unsigned int len = skb_transport_offset(skb) + sizeof(struct igmphdr); 1438 - int ret; 1438 + int ret = -EINVAL; 1439 1439 1440 1440 transport_len = ntohs(ip_hdr(skb)->tot_len) - ip_hdrlen(skb); 1441 1441 1442 - skb_get(skb); 1443 1442 skb_chk = skb_checksum_trimmed(skb, transport_len, 1444 1443 ip_mc_validate_checksum); 1445 1444 if (!skb_chk) 1446 - return -EINVAL; 1445 + goto err; 1447 1446 1448 - if (!pskb_may_pull(skb_chk, len)) { 1449 - kfree_skb(skb_chk); 1450 - return -EINVAL; 1451 - } 1447 + if (!pskb_may_pull(skb_chk, len)) 1448 + goto err; 1452 1449 1453 1450 ret = ip_mc_check_igmp_msg(skb_chk); 1454 - if (ret) { 1455 - kfree_skb(skb_chk); 1456 - return ret; 1457 - } 1451 + if (ret) 1452 + goto err; 1458 1453 1459 1454 if (skb_trimmed) 1460 1455 *skb_trimmed = skb_chk; 1461 - else 1456 + /* free now unneeded clone */ 1457 + else if (skb_chk != skb) 1462 1458 kfree_skb(skb_chk); 1463 1459 1464 - return 0; 1460 + ret = 0; 1461 + 1462 + err: 1463 + if (ret && skb_chk && skb_chk != skb) 1464 + kfree_skb(skb_chk); 1465 + 1466 + return ret; 1465 1467 } 1466 1468 1467 1469 /** ··· 1472 1470 * @skb_trimmed: to store an skb pointer trimmed to IPv4 packet tail (optional) 1473 1471 * 1474 1472 * Checks whether an IPv4 packet is a valid IGMP packet. If so sets 1475 - * skb network and transport headers accordingly and returns zero. 1473 + * skb transport header accordingly and returns zero. 1476 1474 * 1477 1475 * -EINVAL: A broken packet was detected, i.e. it violates some internet 1478 1476 * standard ··· 1487 1485 * to leave the original skb and its full frame unchanged (which might be 1488 1486 * desirable for layer 2 frame jugglers). 1489 1487 * 1490 - * The caller needs to release a reference count from any returned skb_trimmed. 1488 + * Caller needs to set the skb network header and free any returned skb if it 1489 + * differs from the provided skb. 1491 1490 */ 1492 1491 int ip_mc_check_igmp(struct sk_buff *skb, struct sk_buff **skb_trimmed) 1493 1492 {
+18 -15
net/ipv6/mcast_snoop.c
··· 143 143 struct sk_buff *skb_chk = NULL; 144 144 unsigned int transport_len; 145 145 unsigned int len = skb_transport_offset(skb) + sizeof(struct mld_msg); 146 - int ret; 146 + int ret = -EINVAL; 147 147 148 148 transport_len = ntohs(ipv6_hdr(skb)->payload_len); 149 149 transport_len -= skb_transport_offset(skb) - sizeof(struct ipv6hdr); 150 150 151 - skb_get(skb); 152 151 skb_chk = skb_checksum_trimmed(skb, transport_len, 153 152 ipv6_mc_validate_checksum); 154 153 if (!skb_chk) 155 - return -EINVAL; 154 + goto err; 156 155 157 - if (!pskb_may_pull(skb_chk, len)) { 158 - kfree_skb(skb_chk); 159 - return -EINVAL; 160 - } 156 + if (!pskb_may_pull(skb_chk, len)) 157 + goto err; 161 158 162 159 ret = ipv6_mc_check_mld_msg(skb_chk); 163 - if (ret) { 164 - kfree_skb(skb_chk); 165 - return ret; 166 - } 160 + if (ret) 161 + goto err; 167 162 168 163 if (skb_trimmed) 169 164 *skb_trimmed = skb_chk; 170 - else 165 + /* free now unneeded clone */ 166 + else if (skb_chk != skb) 171 167 kfree_skb(skb_chk); 172 168 173 - return 0; 169 + ret = 0; 170 + 171 + err: 172 + if (ret && skb_chk && skb_chk != skb) 173 + kfree_skb(skb_chk); 174 + 175 + return ret; 174 176 } 175 177 176 178 /** ··· 181 179 * @skb_trimmed: to store an skb pointer trimmed to IPv6 packet tail (optional) 182 180 * 183 181 * Checks whether an IPv6 packet is a valid MLD packet. If so sets 184 - * skb network and transport headers accordingly and returns zero. 182 + * skb transport header accordingly and returns zero. 185 183 * 186 184 * -EINVAL: A broken packet was detected, i.e. it violates some internet 187 185 * standard ··· 196 194 * to leave the original skb and its full frame unchanged (which might be 197 195 * desirable for layer 2 frame jugglers). 198 196 * 199 - * The caller needs to release a reference count from any returned skb_trimmed. 197 + * Caller needs to set the skb network header and free any returned skb if it 198 + * differs from the provided skb. 200 199 */ 201 200 int ipv6_mc_check_mld(struct sk_buff *skb, struct sk_buff **skb_trimmed) 202 201 {