Security issue in util-linux: out-of-bounds read in ul_parse_size()
Yashashree Gund <[email protected]> Sun, 1 Feb 2026 14:47:33 +0000
| Newsgroups | org.kernel.vger.util-linux |
|---|---|
| Message-ID | <SEZPR06MB738980A2799CFE3F45B32785E79DA@SEZPR06MB7389.apcprd06.prod.outlook.com> |
Hello util-linux maintainers, I would like to report a security issue in util-linux involving a memory safety flaw in ul_parse_size() that is reachable via libmount during mount option processing. The issue is an out-of-bounds read caused by unchecked lookahead accesses when parsing malformed decimal size strings. The crash is reliably triggered by inputs such as sizelimit=00000.000, which cause ul_parse_size() to read past the end of the input buffer. This occurs even when the input string is properly NUL-terminated. The problem was initially discovered via libFuzzer while fuzzing libmount and was later confirmed with a minimal standalone reproducer that directly calls ul_parse_size(), demonstrating that the issue is intrinsic to the function and not dependent on higher-level parsing behavior. Impact: This bug can lead to process crashes and out-of-bounds memory reads in privileged contexts where util-linux or libmount parse attacker-influenced mount options (e.g., mount helpers, containers, or namespace-aware tools). The issue is therefore security-relevant. Attachments included: * A minimized reproducer input that triggers the crash * The full AddressSanitizer crash report * Execution-path notes describing how the input reaches ul_parse_size() * A minimal standalone reproducer (verify.c) Standalone reproducer details: The attached verify-repro.c is a small test program that directly invokes ul_parse_size() with a crafted decimal size string and checks the result. When built with AddressSanitizer enabled and linked against the util-linux source tree, running the program reliably triggers an out-of-bounds read in ul_parse_size(), confirming the issue independently of libmount or fuzzing infrastructure. Please let me know if you would like the reproducer adapted further or if additional information would be helpful. I am happy to assist with validation or testing of a fix. Thank you for your time and for maintaining util-linux. Best Regards Yashashree Gund
ul_parse_size_asan_report.txt
(text/plain, 7.9 KB)
=================================================================
==290472==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5020008924fa at pc 0xc25d0090fcdc bp 0xffffdcfa9630 sp 0xffffdcfa9628
READ of size 1 at 0x5020008924fa thread T0
#0 0xc25d0090fcd8 in ul_parse_size /home/parallels/util-linux/lib/strutils.c:116:6
#1 0xc25d008d1bd0 in setup_loopdev /home/parallels/util-linux/libmount/src/hook_loopdev.c:170:7
#2 0xc25d008d1bd0 in hook_prepare_loopdev /home/parallels/util-linux/libmount/src/hook_loopdev.c:541:7
#3 0xc25d008b1440 in mnt_context_call_hooks /home/parallels/util-linux/libmount/src/hooks.c:369:9
#4 0xc25d00878384 in mnt_context_prepare_srcpath /home/parallels/util-linux/libmount/src/context.c:2002:7
#5 0xc25d0088adf4 in mnt_context_prepare_mount /home/parallels/util-linux/libmount/src/context_mount.c:792:8
#6 0xc25d0082ab44 in LLVMFuzzerTestOneInput /home/parallels/util-linux/libmount_fuzzer.c:40:27
#7 0xc25d00735c0c in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/parallels/util-linux/libmount_fuzzer_deep+0x225c0c) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#8 0xc25d00735364 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/parallels/util-linux/libmount_fuzzer_deep+0x225364) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#9 0xc25d00736b30 in fuzzer::Fuzzer::MutateAndTestOne() (/home/parallels/util-linux/libmount_fuzzer_deep+0x226b30) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#10 0xc25d00737894 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) (/home/parallels/util-linux/libmount_fuzzer_deep+0x227894) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#11 0xc25d00725730 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/parallels/util-linux/libmount_fuzzer_deep+0x215730) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#12 0xc25d0074e594 in main (/home/parallels/util-linux/libmount_fuzzer_deep+0x23e594) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#13 0xe8fca88884c0 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#14 0xe8fca8888594 in __libc_start_main csu/../csu/libc-start.c:360:3
#15 0xc25d0071ba6c in _start (/home/parallels/util-linux/libmount_fuzzer_deep+0x20ba6c) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
0x5020008924fa is located 0 bytes after 10-byte region [0x5020008924f0,0x5020008924fa)
allocated by thread T0 here:
#0 0xc25d007ed0d4 in malloc (/home/parallels/util-linux/libmount_fuzzer_deep+0x2dd0d4) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#1 0xc25d00763b98 in strndup (/home/parallels/util-linux/libmount_fuzzer_deep+0x253b98) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#2 0xc25d0096e6bc in optlist_new_opt /home/parallels/util-linux/libmount/src/optlist.c:438:16
#3 0xc25d00963424 in optlist_add_optstr /home/parallels/util-linux/libmount/src/optlist.c:524:9
#4 0xc25d00880f84 in apply_fs /home/parallels/util-linux/libmount/src/context.c:2460:8
#5 0xc25d00882a24 in mnt_context_apply_fstab /home/parallels/util-linux/libmount/src/context.c:2610:9
#6 0xc25d0088aa50 in mnt_context_prepare_mount /home/parallels/util-linux/libmount/src/context_mount.c:782:7
#7 0xc25d0082ab44 in LLVMFuzzerTestOneInput /home/parallels/util-linux/libmount_fuzzer.c:40:27
#8 0xc25d00735c0c in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/parallels/util-linux/libmount_fuzzer_deep+0x225c0c) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#9 0xc25d00735364 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/parallels/util-linux/libmount_fuzzer_deep+0x225364) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#10 0xc25d00736b30 in fuzzer::Fuzzer::MutateAndTestOne() (/home/parallels/util-linux/libmount_fuzzer_deep+0x226b30) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#11 0xc25d00737894 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) (/home/parallels/util-linux/libmount_fuzzer_deep+0x227894) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#12 0xc25d00725730 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/parallels/util-linux/libmount_fuzzer_deep+0x215730) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#13 0xc25d0074e594 in main (/home/parallels/util-linux/libmount_fuzzer_deep+0x23e594) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
#14 0xe8fca88884c0 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#15 0xe8fca8888594 in __libc_start_main csu/../csu/libc-start.c:360:3
#16 0xc25d0071ba6c in _start (/home/parallels/util-linux/libmount_fuzzer_deep+0x20ba6c) (BuildId: cd366d821202ca98528bf79b9941e431153e4cc3)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/parallels/util-linux/lib/strutils.c:116:6 in ul_parse_size
Shadow bytes around the buggy address:
0x502000892200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000892280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000892300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000892380: fa fa fa fa fa fa fd fa fa fa fa fa fa fa fa fa
0x502000892400: fa fa fa fa fa fa fd fa fa fa fa fa fa fa fa fa
=>0x502000892480: fa fa fa fa fa fa fd fd fa fa fa fa fa fa 00[02]
0x502000892500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000892580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000892600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000892680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x502000892700: fa fa fa fa fa fa fa fa fa fa fd fd fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==290472==ABORTING
MS: 10 ManualDict-Custom-CopyPart-Custom-ChangeByte-Custom-ShuffleBytes-Custom-CrossOver-Custom- DE: "iversion"-; base unit: aed04bcf8f04c9b420bd06e9bd0054341e9ad91d
0x69,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x31,0x37,0x33,0x20,0x30,0x35,0x64,0x30,0xa,0x37,0x20,0x32,0x35,0x20,0x6e,0x31,0x37,0x33,0x20,0x30,0x35,0x64,0x30,0xa,0x37,0x20,0x72,0xbf,0x67,0x65,0x78,0x65,0xa8,0x20,0x2d,0x20,0x6e,0x66,0x73,0x38,0x20,0xa,0x30,0x35,0x30,0x72,0x2e,0x2e,0x2f,0x2e,0x2f,0x6d,0x28,0x31,0xe4,0x20,0x2f,0x20,0xb2,0x6d,0x70,0x66,0x73,0x20,0xf2,0x2c,0x72,0x77,0x3d,0x22,0x22,0x2c,0x75,0x22,0x22,0x2c,0x2c,0x72,0x77,0x3d,0x22,0x22,0x2c,0x75,0x73,0x65,0x72,0x2c,0x6e,0x6f,0x61,0x75,0x74,0x6f,0x2c,0x58,0x75,0x6d,0x61,0x6b,0x65,0x2d,0x73,0x68,0x61,0x72,0x65,0x64,0x22,0x22,0x2c,0x73,0x69,0x7a,0x65,0x6c,0x69,0x6d,0x69,0x74,0x3d,0x30,0x30,0x30,0x30,0x30,0x2e,0x30,0x30,0x30,0xa,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x31,0x30,0x33,0x30,0x36,0x32,0x32,0x36,0x31,0x36,0x2c,0x68,0x65,0x6c,0x70,0x2c,0x64,0x69,0x32,
iversion173 05d0\0127 25 n173 05d0\0127 r\277gexe\250 - nfs8 \012050r.././m(1\344 / \262mpfs \362,rw=\"\",u\"\",,rw=\"\",user,noauto,Xumake-shared\"\",sizelimit=00000.000\012000000001030622616,help,di2
artifact_prefix='./'; Test unit written to ./crash-eecdf3b60e9e72795b3cea62b63d59a61b770c42
Base64: aXZlcnNpb24xNzMgMDVkMAo3IDI1IG4xNzMgMDVkMAo3IHK/Z2V4ZaggLSBuZnM4IAowNTByLi4vLi9tKDHkIC8gsm1wZnMg8ixydz0iIix1IiIsLHJ3PSIiLHVzZXIsbm9hdXRvLFh1bWFrZS1zaGFyZWQiIixzaXplbGltaXQ9MDAwMDAuMDAwCjAwMDAwMDAwMTAzMDYyMjYxNixoZWxwLGRpMg==
verify-repro.c
(text/plain, 293 B)
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include "strutils.h"
int main(void)
{
uintmax_t res;
const char *s = "00000.000";
int rc = ul_parse_size(s, &res, NULL);
printf("input='%s' rc=%d errno=%d res=%ju\n",
s, rc, errno, res);
return 0;
}
ul_parse_size_execution_path.txt
(text/plain, 1.3 KB)
Execution path notes – ul_parse_size heap OOB read
Overview
The attached reproducer input triggers a heap out-of-bounds read in
ul_parse_size() during mount option parsing in libmount.
High-level flow
The reproducer input is consumed by libmount as part of mount option
processing (e.g. via options such as sizelimit=).
libmount parses mount options into an option list.
Size-related options are passed to ul_parse_size() for numeric parsing.
Call path (trimmed)
mnt_context_prepare_mount()
└─ mnt_context_apply_fstab()
└─ apply_fs()
└─ optlist_add_optstr()
└─ optlist_new_opt()
└─ ul_parse_size()
Fault details
Input value example: sizelimit=00000.000
Inside ul_parse_size():
strtoumax() parses the integer part and sets end to the decimal point.
Decimal parsing advances the pointer p to the last fractional digit.
The suffix-checking logic then unconditionally dereferences p + 1
(and potentially further offsets).
When p points to the final byte of a heap-allocated string (allocated via
strndup()), dereferencing p + 1 results in a heap out-of-bounds read.
Result
AddressSanitizer reports a reproducible heap buffer overflow (read of size 1)
in ul_parse_size(). The issue is input-driven and does not depend on
uninitialized memory or use-after-free conditions.