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

netconsole: Fix race condition in between reader and writer of userdata

The update_userdata() function constructs the complete userdata string
in nt->extradata_complete and updates nt->userdata_length. This data
is then read by write_msg() and write_ext_msg() when sending netconsole
messages. However, update_userdata() was not holding target_list_lock
during this process, allowing concurrent message transmission to read
partially updated userdata.

This race condition could result in netconsole messages containing
incomplete or inconsistent userdata - for example, reading the old
userdata_length with new extradata_complete content, or vice versa,
leading to truncated or corrupted output.

Fix this by acquiring target_list_lock with spin_lock_irqsave() before
updating extradata_complete and userdata_length, and releasing it after
both fields are fully updated. This ensures that readers see a
consistent view of the userdata, preventing corruption during concurrent
access.

The fix aligns with the existing locking pattern used throughout the
netconsole code, where target_list_lock protects access to target
fields including buf[] and msgcounter that are accessed during message
transmission.

Also get rid of the unnecessary variable complete_idx, which makes it
easier to bail out of update_userdata().

Fixes: df03f830d099 ("net: netconsole: cache userdata formatted string in netconsole_target")
Signed-off-by: Gustavo Luiz Duarte <gustavold@gmail.com>
Link: https://patch.msgid.link/20251028-netconsole-fix-race-v4-1-63560b0ae1a0@meta.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>

authored by

Gustavo Luiz Duarte and committed by
Jakub Kicinski
00764aa5 a4330380

+13 -8
+13 -8
drivers/net/netconsole.c
··· 886 886 887 887 static void update_userdata(struct netconsole_target *nt) 888 888 { 889 - int complete_idx = 0, child_count = 0; 890 889 struct list_head *entry; 890 + int child_count = 0; 891 + unsigned long flags; 892 + 893 + spin_lock_irqsave(&target_list_lock, flags); 891 894 892 895 /* Clear the current string in case the last userdatum was deleted */ 893 896 nt->userdata_length = 0; ··· 900 897 struct userdatum *udm_item; 901 898 struct config_item *item; 902 899 903 - if (WARN_ON_ONCE(child_count >= MAX_EXTRADATA_ITEMS)) 904 - break; 900 + if (child_count >= MAX_EXTRADATA_ITEMS) { 901 + spin_unlock_irqrestore(&target_list_lock, flags); 902 + WARN_ON_ONCE(1); 903 + return; 904 + } 905 905 child_count++; 906 906 907 907 item = container_of(entry, struct config_item, ci_entry); ··· 918 912 * one entry length (1/MAX_EXTRADATA_ITEMS long), entry count is 919 913 * checked to not exceed MAX items with child_count above 920 914 */ 921 - complete_idx += scnprintf(&nt->extradata_complete[complete_idx], 922 - MAX_EXTRADATA_ENTRY_LEN, " %s=%s\n", 923 - item->ci_name, udm_item->value); 915 + nt->userdata_length += scnprintf(&nt->extradata_complete[nt->userdata_length], 916 + MAX_EXTRADATA_ENTRY_LEN, " %s=%s\n", 917 + item->ci_name, udm_item->value); 924 918 } 925 - nt->userdata_length = strnlen(nt->extradata_complete, 926 - sizeof(nt->extradata_complete)); 919 + spin_unlock_irqrestore(&target_list_lock, flags); 927 920 } 928 921 929 922 static ssize_t userdatum_value_store(struct config_item *item, const char *buf,