CVE-2024-53680
Publication date:
11/01/2025
In the Linux kernel, the following vulnerability has been resolved:<br />
<br />
ipvs: fix UB due to uninitialized stack access in ip_vs_protocol_init()<br />
<br />
Under certain kernel configurations when building with Clang/LLVM, the<br />
compiler does not generate a return or jump as the terminator<br />
instruction for ip_vs_protocol_init(), triggering the following objtool<br />
warning during build time:<br />
<br />
vmlinux.o: warning: objtool: ip_vs_protocol_init() falls through to next function __initstub__kmod_ip_vs_rr__935_123_ip_vs_rr_init6()<br />
<br />
At runtime, this either causes an oops when trying to load the ipvs<br />
module or a boot-time panic if ipvs is built-in. This same issue has<br />
been reported by the Intel kernel test robot previously.<br />
<br />
Digging deeper into both LLVM and the kernel code reveals this to be a<br />
undefined behavior problem. ip_vs_protocol_init() uses a on-stack buffer<br />
of 64 chars to store the registered protocol names and leaves it<br />
uninitialized after definition. The function calls strnlen() when<br />
concatenating protocol names into the buffer. With CONFIG_FORTIFY_SOURCE<br />
strnlen() performs an extra step to check whether the last byte of the<br />
input char buffer is a null character (commit 3009f891bb9f ("fortify:<br />
Allow strlen() and strnlen() to pass compile-time known lengths")).<br />
This, together with possibly other configurations, cause the following<br />
IR to be generated:<br />
<br />
define hidden i32 @ip_vs_protocol_init() local_unnamed_addr #5 section ".init.text" align 16 !kcfi_type !29 {<br />
%1 = alloca [64 x i8], align 16<br />
...<br />
<br />
14: ; preds = %11<br />
%15 = getelementptr inbounds i8, ptr %1, i64 63<br />
%16 = load i8, ptr %15, align 1<br />
%17 = tail call i1 @llvm.is.constant.i8(i8 %16)<br />
%18 = icmp eq i8 %16, 0<br />
%19 = select i1 %17, i1 %18, i1 false<br />
br i1 %19, label %20, label %23<br />
<br />
20: ; preds = %14<br />
%21 = call i64 @strlen(ptr noundef nonnull dereferenceable(1) %1) #23<br />
...<br />
<br />
23: ; preds = %14, %11, %20<br />
%24 = call i64 @strnlen(ptr noundef nonnull dereferenceable(1) %1, i64 noundef 64) #24<br />
...<br />
}<br />
<br />
The above code calculates the address of the last char in the buffer<br />
(value %15) and then loads from it (value %16). Because the buffer is<br />
never initialized, the LLVM GVN pass marks value %16 as undefined:<br />
<br />
%13 = getelementptr inbounds i8, ptr %1, i64 63<br />
br i1 undef, label %14, label %17<br />
<br />
This gives later passes (SCCP, in particular) more DCE opportunities by<br />
propagating the undef value further, and eventually removes everything<br />
after the load on the uninitialized stack location:<br />
<br />
define hidden i32 @ip_vs_protocol_init() local_unnamed_addr #0 section ".init.text" align 16 !kcfi_type !11 {<br />
%1 = alloca [64 x i8], align 16<br />
...<br />
<br />
12: ; preds = %11<br />
%13 = getelementptr inbounds i8, ptr %1, i64 63<br />
unreachable<br />
}<br />
<br />
In this way, the generated native code will just fall through to the<br />
next function, as LLVM does not generate any code for the unreachable IR<br />
instruction and leaves the function without a terminator.<br />
<br />
Zero the on-stack buffer to avoid this possible UB.
Severity CVSS v4.0: Pending analysis
Last modification:
03/11/2025