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.