From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-pg1-f176.google.com (mail-pg1-f176.google.com [209.85.215.176]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 132CF3783AB for ; Tue, 24 Feb 2026 10:23:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.215.176 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1771928624; cv=none; b=YwXvwoL4uuvZ4FckEEHi+53etw5HunrunnEbnZhveGBo4Ovb+q3CMffyGEUF1Am0nUIzR8PYDTV6m8VesZMYqnELEt3sISeH+vhfIlqrsr4KrmfOt9pa/aMwTxKSTFIb/oqy4IKGNQe3/15/4LB0b7hP46N1gfze7k41NJGN710= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1771928624; c=relaxed/simple; bh=421syxCBslsS4bjsDoEo+2+tvGKAp6+RYQRGj/n5jFs=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=Ca0tgWUkauHDb6xV9H1XS7uafHklEZO4ReA+B6NoAUHOhKwTLBqDUYCfzc+Dp7ZUhIc/5Rn2Ggto6DO/ge0GtWSDzfaWR+vzgk8RjhyN3SMFmKzgTTzQrC3nPFJOKb9tNQYQ1XVkpjN+G4Rp8TLWSfXud/snM0t+lrWi6DqbouE= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=BTT+RLpc; arc=none smtp.client-ip=209.85.215.176 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="BTT+RLpc" Received: by mail-pg1-f176.google.com with SMTP id 41be03b00d2f7-c648bc907ebso3121611a12.3 for ; Tue, 24 Feb 2026 02:23:41 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1771928621; x=1772533421; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=q8zTGRllPWTMhAthlkfM6Ojar/mRF5jc6r9gUsQ67os=; b=BTT+RLpcol9A4gQLq+GFhmepZkkJTn2R9Fa7fsK6qdSug7LTmSjKY7WfAd9IM3vKhg x5hKYgAN+28/LNmNQSSysl1gZLoHp/s5BomsEx/HnMvhAUY4ZBbQj/GgCB8q2YhCjW+E uur2K9V8jqFEkPkng4jvub5nteh1CwWeLO9+M5kHG5vNde+UzKvMW65dCmcX+aI2fyuf sVYRBFdpU+YgnLnWnoKFQPamgjsSTnhO0YZ6bTglYPvgkSqVlqZIGdnS5Ot4SrBPDvdP Ys7tIu8BToywTqsD86RJcNBmApjmGeLYqSf8DSgl3TvKaeG+7g5I5xXKp5ldc1zhA8sY GcAw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1771928621; x=1772533421; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=q8zTGRllPWTMhAthlkfM6Ojar/mRF5jc6r9gUsQ67os=; b=nKF/puWwf8tBFfWXzuRDkCM15QCV5v/XYS7IrkUsRJUKywT9qZbNf9WhoJZ8TqiJTR ZvpuvL2Iw4GN5rH1WIsIpvv5ArR3A2hNLYC364V4KrFkBtAsX8SwVbmU9CAXyU1O0AH1 gWi0LAFQfSePHOCiC7ncyxy0LCmpD/6S5rbTYnXwgfY9oXamzqJkUDDRuyt515TFh2D2 O0bz80eyZiZbf0psrsgYQ4E3ByU10z4zUuXnPZ5NP5H1o8ioTMaY/pwXCEyQYLypAD4w FcrNMLhg5CxrbnZxLbS3AuYmf9Ja4Zf947iu9rJufEzT3ARswOM/1HoTB/e54lJXOlId XXsQ== X-Gm-Message-State: AOJu0YzGxJyegxKQnloBpp+JEdhYEvIRxGTn99tmE3M8GXZLX6lUEPsE vHeZWEgcJ7tyV2hZqjRfK3gkbK4nYRbVSHT1cFD19MgdDooNPC8eov5YQ5g774Th X-Gm-Gg: ATEYQzwO05I6iJhTqAdieB+DBvgJagtbPwHWPbNlN8EcwX6e/gFJPpcMSURSdsRe/wX 0uCut0l1+jLvjRRyUGyh+MS/HshiPT14ErCH7fRxJJ2oZbdPocGzv7+BUsU/n65CtwmyZH4pBKl S9/N4i6WRglJIt0SE4Ap8VDVeMPoyq9TFVXLK8d1PO4LU5Goor3FvlkBnxoQ2jLDmH8kW5iXnkW hr9JiWFlfPEC6OzC+BiCwW/E8Oa5pYhno8W0anZRjufxZuYKFOulSU1p2sTSoGcNfQsNltuFHx+ 1ow+kN+J/I36Vu2eRuoJQL1NUtYLsDUxVng0EFXYry4S6xNrIThJvcrIDLIJrjm+s/C94nLiCEr +KqrnrCbSURty7iyRTRZnLJIO7dNJPnUuGO22khUGXAn6YRopOd+GOY6oW0DAZNf2P7lTzkhINt QldKN3uGnqM+IQ9/UgrDBBUjL+IAK7oj5EHg7p3nTIEcK5OGFi9KrXrDvIhM6GEeCo/so8oLPJ3 nRVyrWukgcdeb6MOg== X-Received: by 2002:a17:902:d2d2:b0:2ad:ad65:7df1 with SMTP id d9443c01a7336-2adad658681mr7702765ad.19.1771928620082; Tue, 24 Feb 2026 02:23:40 -0800 (PST) Received: from localhost.localdomain ([2601:1c0:4c01:5c50::758c]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-2ad750275d0sm127637365ad.61.2026.02.24.02.23.39 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 24 Feb 2026 02:23:39 -0800 (PST) From: Jonathan Gruber To: util-linux@vger.kernel.org Cc: Jonathan Gruber Subject: [PATCH] Various subsystems: avoid UB from ref counts Date: Tue, 24 Feb 2026 02:23:29 -0800 Message-ID: <20260224102329.69255-1-jonathan.gruber.jg@gmail.com> X-Mailer: git-send-email 2.53.0 Precedence: bulk X-Mailing-List: util-linux@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit util-linux uses type signed int for reference counters. The functions that increment reference counters thus risk undefined behavior (UB) if the value of the reference counter is INT_MAX prior to the increment, because overflow in signed integers is UB. A program that creates more than INT_MAX coexistent references to some reference-counted object would thus trigger UB by overflowing the reference counter over INT_MAX. On systems with size_t larger than int, such as many 64-bit systems, it is possible, given sufficient RAM, for a program to create more than INT_MAX pointers holding the address of the same reference-counted object and to attempt to make each of those pointers be a reference to the reference-counted object, thereby overflowing the reference counter over INT_MAX and triggering UB. It is best to protect against the demonstrated *possibility* of this UB-triggering scenario regardless of the scenario's probability. While signed integer overflow is UB, it is likely to manifest in this scenario as the reference counter wrapping from INT_MAX to INT_MIN. However, the functions that decrement the reference counters deallocate the reference-counted object if the value of the reference counter becomes less than or equal to zero after the decrement. Therefore, if the reference counter starts with a value of one and is incremented INT_MAX + 2 times with no intervening decrements, then, assuming that the resulting integer overflow manifests as a wrap from INT_MAX to INT_MIN, the value of the reference counter would become INT_MIN + 2. If the reference counter is then decremented once, then its value would become INT_MIN + 1, which is less than zero, so the reference-counted object would be deallocated even though there are still 1 + (INT_MAX + 2) - 1 = INT_MAX + 2 live references to the object. Since the object has been deallocated, any access to the remaining references would trigger UB. Additionally, if the reference counter is *attempted* to be decremented again (which would trigger UB since the reference-counted object has been deallocated), then, assuming that the memory formerly occupied by the reference-counted object has somehow not been reallocated to another object or overwritten, and assuming that the UB from the attempted decrement manifests as an actual decrement, the "value" of the (deallocated) reference counter would become INT_MIN, which is less than zero, so the already-deallocated reference-counted object would be deallocated again, resulting in a double free, which also triggers UB. This patch attempts to address the UB from the reference counters. In order to avoid UB from overflowing the reference counters, this patch changes the types of the reference counters to an unsigned integer type, since overflow for unsigned integer types is not UB but wraps; in this case, if the reference counter overflows, then its value would wrap to zero. In order to avoid the UB resulting from deallocating the reference-counted object when there are still live references to the object, this patch ensures that the value of the reference counter is zero only if there are actually no live references to the reference-counted object. This is done by calling abort() in the functions that increment reference counters when the reference counter overflows; this behavior is also documented in the documentation comments for the functions. The choice to call abort() was made for simplicity and because the *least bad* result of accessing a reference counted object that has been deallocated too early is for the program to crash, which is also what calling abort() triggers. Finally, the unsigned integer type that was chosen for the reference counters is size_t; this choice minimizes the risk of overflowing the reference counter and triggering an abort() because storing more than SIZE_MAX coexistent pointers/references to a reference-counted object would require more than SIZE_MAX bytes of memory. size_t can be larger than int, so this patch might increase the sizes of some reference-counted objects, but this increase should be miniscule. Signed-off-by: Jonathan Gruber --- include/path.h | 2 +- lib/path.c | 22 +++++++++++++++-- libfdisk/src/ask.c | 18 +++++++++++--- libfdisk/src/context.c | 18 ++++++++++++-- libfdisk/src/fdiskP.h | 15 ++++++------ libfdisk/src/item.c | 16 +++++++++++-- libfdisk/src/partition.c | 19 ++++++++++++--- libfdisk/src/parttype.c | 16 +++++++++++-- libfdisk/src/script.c | 19 ++++++++++++--- libfdisk/src/table.c | 17 +++++++++++-- libmount/src/cache.c | 20 ++++++++++++---- libmount/src/fs.c | 21 ++++++++++++---- libmount/src/fs_statmount.c | 18 +++++++++++--- libmount/src/lock.c | 21 ++++++++++++---- libmount/src/monitor.c | 16 +++++++++++-- libmount/src/monitor.h | 3 ++- libmount/src/mountP.h | 10 ++++---- libmount/src/optlist.c | 23 ++++++++++++++++-- libmount/src/tab.c | 19 +++++++++++---- libsmartcols/src/column.c | 15 ++++++++++-- libsmartcols/src/filter.c | 34 +++++++++++++++++++++++--- libsmartcols/src/grouping.c | 24 +++++++++++++++++-- libsmartcols/src/line.c | 15 ++++++++++-- libsmartcols/src/smartcolsP.h | 18 +++++++------- libsmartcols/src/symbols.c | 15 ++++++++++-- libsmartcols/src/table.c | 15 ++++++++++-- misc-utils/lsblk-devtree.c | 45 +++++++++++++++++++++++++++++++---- misc-utils/lsblk.h | 4 ++-- sys-utils/lscpu-cpu.c | 23 ++++++++++++++++-- sys-utils/lscpu-cputype.c | 23 +++++++++++++++--- sys-utils/lscpu.h | 5 ++-- 31 files changed, 454 insertions(+), 95 deletions(-) diff --git a/include/path.h b/include/path.h index 60a6162df..1b403d66e 100644 --- a/include/path.h +++ b/include/path.h @@ -17,7 +17,7 @@ struct path_cxt { int dir_fd; char *dir_path; - int refcount; + size_t refcount; char *prefix; char path_buffer[PATH_MAX]; diff --git a/lib/path.c b/lib/path.c index 6ce8a10d2..10aaa7b5c 100644 --- a/lib/path.c +++ b/lib/path.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -79,10 +80,27 @@ fail: return NULL; } +/** + * ul_ref_path: + * @pc: path_cxt instance + * + * Increments reference counter. + * Calls abort() if reference counter equals SIZE_MAX prior to the increment. + */ void ul_ref_path(struct path_cxt *pc) { - if (pc) + if (pc) { pc->refcount++; + /* + * pc->refcount == 0 now if and only if + * pc->refcount == SIZE_MAX prior to the increment. + * + * Checking for overflow after the increment instead of before + * seems to generate better assembly. + */ + if (pc->refcount == 0) + abort(); + } } void ul_unref_path(struct path_cxt *pc) @@ -92,7 +110,7 @@ void ul_unref_path(struct path_cxt *pc) pc->refcount--; - if (pc->refcount <= 0) { + if (pc->refcount == 0) { DBG(CXT, ul_debugobj(pc, "dealloc")); if (pc->dialect) pc->free_dialect(pc); diff --git a/libfdisk/src/ask.c b/libfdisk/src/ask.c index 507cc6fc6..3ce472aa4 100644 --- a/libfdisk/src/ask.c +++ b/libfdisk/src/ask.c @@ -3,6 +3,7 @@ #include "fdiskP.h" #include +#include /** * SECTION: ask @@ -49,7 +50,7 @@ struct fdisk_ask *fdisk_new_ask(void) void fdisk_reset_ask(struct fdisk_ask *ask) { - int refcount; + size_t refcount; assert(ask); free(ask->query); @@ -69,11 +70,22 @@ void fdisk_reset_ask(struct fdisk_ask *ask) * @ask: ask instance * * Increments reference counter. + * Calls abort() if reference counter equals SIZE_MAX prior to the increment. */ void fdisk_ref_ask(struct fdisk_ask *ask) { - if (ask) + if (ask) { ask->refcount++; + /* + * ask->refcount == 0 now if and only if + * ask->refcount == SIZE_MAX prior to the increment. + * + * Checking for overflow after the increment instead of before + * seems to generate better assembly. + */ + if (ask->refcount == 0) + abort(); + } } @@ -90,7 +102,7 @@ void fdisk_unref_ask(struct fdisk_ask *ask) return; ask->refcount--; - if (ask->refcount <= 0) { + if (ask->refcount == 0) { fdisk_reset_ask(ask); DBG(ASK, ul_debugobj(ask, "free")); free(ask); diff --git a/libfdisk/src/context.c b/libfdisk/src/context.c index bc46f1f62..6d8faa8ff 100644 --- a/libfdisk/src/context.c +++ b/libfdisk/src/context.c @@ -1,3 +1,6 @@ + +#include + #ifdef HAVE_LIBBLKID # include #endif @@ -199,11 +202,22 @@ struct fdisk_context *fdisk_new_nested_context(struct fdisk_context *parent, * @cxt: context pointer * * Increments reference counter. + * Calls abort() if reference counter equals SIZE_MAX prior to the increment. */ void fdisk_ref_context(struct fdisk_context *cxt) { - if (cxt) + if (cxt) { cxt->refcount++; + /* + * ctx->refcount == 0 now if and only if + * ctx->refcount == SIZE_MAX prior to the increment. + * + * Checking for overflow after the increment instead of before + * seems to generate better assembly. + */ + if (cxt->refcount == 0) + abort(); + } } /** @@ -1089,7 +1103,7 @@ void fdisk_unref_context(struct fdisk_context *cxt) return; cxt->refcount--; - if (cxt->refcount <= 0) { + if (cxt->refcount == 0) { DBG(CXT, ul_debugobj(cxt, "freeing context %p for %s", cxt, cxt->dev_path)); reset_context(cxt); /* this is sensitive to parent<->child relationship! */ diff --git a/libfdisk/src/fdiskP.h b/libfdisk/src/fdiskP.h index e3e57929a..e1af40588 100644 --- a/libfdisk/src/fdiskP.h +++ b/libfdisk/src/fdiskP.h @@ -107,12 +107,13 @@ struct fdisk_iter { * Partition types */ struct fdisk_parttype { - unsigned int code; /* type as number or zero */ + size_t refcount; /* reference counter for allocated types */ + char *name; /* description */ char *typestr; /* type as string or NULL */ + unsigned int code; /* type as number or zero */ unsigned int flags; /* FDISK_PARTTYPE_* flags */ - int refcount; /* reference counter for allocated types */ }; enum { @@ -136,7 +137,7 @@ struct fdisk_shortcut { }; struct fdisk_partition { - int refcount; /* reference counter */ + size_t refcount; /* reference counter */ size_t partno; /* partition number */ size_t parent_partno; /* for logical partitions */ @@ -200,7 +201,7 @@ enum { struct fdisk_table { struct list_head parts; /* partitions */ - int refcount; + size_t refcount; size_t nents; /* number of partitions */ }; @@ -340,7 +341,7 @@ struct fdisk_ask { int type; /* FDISK_ASKTYPE_* */ char *query; - int refcount; + size_t refcount; union { /* FDISK_ASKTYPE_{NUMBER,OFFSET} */ @@ -384,7 +385,7 @@ struct fdisk_context { char *dev_model; /* on linux /sys/block//device/model or NULL */ struct stat dev_st; /* stat(2) result */ - int refcount; + size_t refcount; unsigned char *firstsector; /* buffer with master boot record */ unsigned long firstsector_bufsz; @@ -484,7 +485,7 @@ extern int fdisk_probe_labels(struct fdisk_context *cxt); extern void fdisk_deinit_label(struct fdisk_label *lb); struct fdisk_labelitem { - int refcount; /* reference counter */ + size_t refcount; /* reference counter */ int id; /*