From 805d010714b074b8d2f87923fd211ea65d057f1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9Cwengjianing=E2=80=9D?= <1528193783@qq.com>
Date: Thu, 3 Sep 2026 20:23:54 +0800
Subject: [PATCH] dd: add pre-write safety check for block devices

if dd is used to write block device, would destroy in-use devices,
or LVM,  partition tables on devices.  Before opening the output,
probe with libblkid and warn if content is recognized, prompting
for confirmation.

* configure.ac: Check for blkid/blkid.h and libblkid; auto-detected,
can be disabled with --without-blkid.
* src/local.mk (src_dd_LDADD): Link dd with $(BLKID_LIBS).
* src/dd.c: Include <blkid/blkid.h>.
(device_is_mounted, probe_output_content, describe_output_type,
confirm_output_overwrite, warn_if_output_in_use): New functions.
(main): Call warn_if_output_in_use() before opening the output.
* NEWS: Mention the new safety check.
---
 NEWS         |   7 ++
 configure.ac |  29 +++++++
 src/dd.c     | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/local.mk |   2 +-
 4 files changed, 253 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 0e4ad1d94..744429f9c 100644
--- a/NEWS
+++ b/NEWS
@@ -99,6 +99,13 @@ GNU coreutils NEWS                                    -*- outline -*-
 
 ** New Features
 
+  dd now warns and asks for confirmation before writing to a block device
+  that is mounted, or whose contents look like a file system, an LVM physical
+  volume, or a partition table, as detected with libblkid.  This helps avoid
+  accidentally destroying a disk, e.g. with a mistaken of= argument.  The check
+  applies only in interactive use, and any failure to probe counts as "nothing
+  detected", so it can never make dd fail on its own.
+
   'env' now supports --env0-from=FILE to read NUL-delimited environment entries
   from a file.  With -i, entries are preserved exactly, allowing full
   round-tripping of environments containing duplicate or nonstandard entries.
diff --git a/configure.ac b/configure.ac
index 025c3edc6..0d109475f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -376,6 +376,35 @@ if test $ac_cv_func_syslog = no; then
   done
 fi
 
+# libblkid, for dd's output-device safety check.  It is optional:
+# without it, dd is simply built without the check.  Honor
+# --without-blkid to force-disable and --with-blkid to force-enable
+# (the latter failing if libblkid is not found).
+AC_ARG_WITH([blkid],
+  [AS_HELP_STRING([--without-blkid],
+     [do not build dd's output-device safety check, even if libblkid is available])],
+  [], [with_blkid=auto])
+if test "$with_blkid" != no; then
+  ac_save_LIBS=$LIBS
+  AC_CHECK_HEADER([blkid/blkid.h])
+  AC_CHECK_LIB([blkid], [blkid_new_probe])
+  LIBS=$ac_save_LIBS
+  if test "$ac_cv_header_blkid_blkid_h" = yes \
+     && test "$ac_cv_lib_blkid_blkid_new_probe" = yes; then
+    BLKID_LIBS=-lblkid
+    AC_DEFINE([HAVE_BLKID], [1],
+      [Define to 1 if you have the blkid library and <blkid/blkid.h>.])
+  elif test "$with_blkid" = yes; then
+    AC_MSG_ERROR([--with-blkid given, but libblkid was not found])
+  fi
+fi
+if test "$BLKID_LIBS"; then
+  AC_MSG_NOTICE([dd: block-device safety check enabled])
+else
+  AC_MSG_NOTICE([dd: block-device safety check disabled (libblkid not found)])
+fi
+AC_SUBST([BLKID_LIBS])
+
 AC_CACHE_CHECK([for 3-argument setpriority function],
   [utils_cv_func_setpriority],
   [AC_LINK_IFELSE(
diff --git a/src/dd.c b/src/dd.c
index 26382a233..94c8010a9 100644
--- a/src/dd.c
+++ b/src/dd.c
@@ -21,6 +21,10 @@
 #include <sys/types.h>
 #include <signal.h>
 
+#ifdef HAVE_BLKID
+# include <blkid/blkid.h>
+#endif
+
 #include "system.h"
 #include "alignalloc.h"
 #include "close-stream.h"
@@ -2425,6 +2429,212 @@ synchronize_output (void)
   return exit_status;
 }
 
+#ifdef HAVE_BLKID
+
+/* Advisory safety check for the output of dd:  if the output is a
+   block device that is in use, or whose contents look like a file
+   system, an LVM physical volume, or a partition table, warn the
+   user and ask for confirmation before any byte is written.  Every
+   failure to probe counts as "nothing detected", so this check can
+   never make dd fail on its own.  The content probing is done by
+   libblkid, which also recognizes the LVM label on logical volumes,
+   as LVM supports stacking a new physical volume on top of one.  */
+
+/* Human-readable descriptions for the signatures libblkid reports on
+   a block device that dd is about to overwrite.  */
+static struct output_type_description
+{
+  char const *type;           /* libblkid TYPE or PTTYPE value.  */
+  char const *desc;           /* What to report if TYPE matches.  */
+} const output_type_descriptions[] =
+{
+  { "LVM2_member", N_("an LVM physical volume") },
+  { "LUKS", N_("a LUKS encrypted volume") },
+  { "LUKS2", N_("a LUKS encrypted volume") },
+  { "swap", N_("a swap area") },
+  { "linux_raid_member", N_("a Linux RAID member device") },
+  { "ext2", N_("an ext2 file system") },
+  { "ext3", N_("an ext3 file system") },
+  { "ext4", N_("an ext4 file system") },
+  { "xfs", N_("an XFS file system") },
+  { "btrfs", N_("a btrfs file system") },
+  { "vfat", N_("a FAT file system") },
+  { "exfat", N_("an exFAT file system") },
+  { "ntfs", N_("an NTFS file system") },
+  { "f2fs", N_("an F2FS file system") },
+  { "jfs", N_("a JFS file system") },
+  { "reiserfs", N_("a ReiserFS file system") },
+  { "iso9660", N_("an ISO 9660 file system") },
+  { "udf", N_("a UDF file system") },
+  { "zfs", N_("a ZFS file system") },
+  { "zfs_member", N_("a ZFS member device") },
+  { "dos", N_("an MBR partition table") },
+  { "gpt", N_("a GPT partition table") },
+  { "sun", N_("a Sun partition table") },
+  { "sgi", N_("an SGI partition table") },
+  { "mac", N_("a Mac partition table") },
+  { "amiga", N_("an Amiga partition table") },
+  { "bsd", N_("a BSD partition table") },
+  { "aix", N_("an AIX partition table") },
+  { NULL, NULL }
+};
+
+/* Return a description of what libblkid identified on the device,
+   given the TYPE it reported (a libblkid TYPE or PTTYPE value).  */
+static char const *
+describe_output_type (char const *type)
+{
+  for (struct output_type_description const *t = output_type_descriptions;
+       t->type; t++)
+    if (strcmp (t->type, type) == 0)
+      return _(t->desc);
+
+  /* Unknown types still deserve a warning; report the raw type.  */
+  static char desc[128];
+  snprintf (desc, sizeof desc, _("data of type '%s'"), type);
+  return desc;
+}
+
+/* Return what the block device open on PROBE_FD contains, judging
+   from libblkid's superblock and partition-table signatures, or
+   NULL if nothing is recognized.  A partition table is reported
+   in preference to a file system.  Any probing failure counts as
+   "nothing detected".  */
+static char const *
+probe_output_content (int probe_fd)
+{
+  char const *found = NULL;
+  blkid_probe pr = blkid_new_probe ();
+  if (! pr)
+    return NULL;
+
+  /* libblkid probing results are compared against plain integers
+     rather than the BLKID_PROBE_* macros, as those macros are only
+     available in libblkid >= 2.39 while the return-value semantics
+     (0 = signature found) are stable across versions.  */
+  if (blkid_probe_set_device (pr, probe_fd, 0, 0) == 0
+      && blkid_probe_enable_superblocks (pr, 1) == 0
+      && blkid_probe_enable_partitions (pr, 1) == 0
+      && blkid_do_safeprobe (pr) == 0)
+    {
+      char const *type = NULL;
+      if (blkid_probe_lookup_value (pr, "PTTYPE", &type, NULL) != 0)
+        blkid_probe_lookup_value (pr, "TYPE", &type, NULL);
+      found = type ? describe_output_type (type) : NULL;
+    }
+
+  /* The probe does not own PROBE_FD, so closing it here is safe.  */
+  blkid_free_probe (pr);
+  return found;
+}
+
+/* Return true if the block device with id RDEV is mounted, judging
+   from /proc/self/mountinfo.  */
+static bool
+device_is_mounted (dev_t rdev)
+{
+  FILE *fp = fopen ("/proc/self/mountinfo", "r");
+  if (! fp)
+    return false;
+
+  bool found = false;
+  char *line = NULL;
+  size_t line_alloc = 0;
+  while (getline (&line, &line_alloc, fp) >= 0)
+    {
+      unsigned int maj, min;
+      if (sscanf (line, "%*u %*u %u:%u", &maj, &min) == 2
+          && makedev (maj, min) == rdev)
+        {
+          found = true;
+          break;
+        }
+    }
+  free (line);
+  fclose (fp);
+  return found;
+}
+
+/* Ask on the controlling terminal whether to continue.  Return
+   true for an explicit "y" answer or for an empty line, which
+   means the default; any other answer, EOF, or an unavailable
+   terminal means no.  Read the answer from /dev/tty rather than
+   from stdin, which carries the data to copy.  */
+static bool
+confirm_output_overwrite (void)
+{
+  fputs (_("Proceed anyway? (y/N) "), stderr);
+  fflush (stderr);
+
+  FILE *tty = fopen ("/dev/tty", "r");
+  if (! tty)
+    return true;
+
+  char answer[8];
+  char *s = fgets (answer, sizeof answer, tty);
+  fclose (tty);
+  return s && (s[0] == '\n' || s[0] == 'y' || s[0] == 'Y');
+}
+
+/* Warn and ask for confirmation if FILE, the output of dd, is a
+   block device that is in use or already contains a file system,
+   an LVM physical volume, or a partition table.  Exit on refusal,
+   before any byte is written to FILE.  Return silently if nothing
+   is detected or if probing is not possible.  */
+static void
+warn_if_output_in_use (char const *file)
+{
+  struct stat probe_stat;
+  if (stat (file, &probe_stat) != 0
+      || ! S_ISBLK (probe_stat.st_mode))
+    return;
+
+  /* The device is opened read-only on a separate descriptor so
+     that probing does not disturb the descriptor dd writes to;
+     O_NONBLOCK guards against the file having become a FIFO in
+     the race after the stat call above.  */
+  int probe_fd = open (file, O_RDONLY | O_NONBLOCK);
+  if (probe_fd < 0)
+    return;
+
+  if (ifstat (probe_fd, &probe_stat) == 0
+      && S_ISBLK (probe_stat.st_mode))
+    {
+      /* A mounted device is in use; warn without probing further.  */
+      bool mounted = device_is_mounted (probe_stat.st_rdev);
+      char const *found = NULL;
+      if (! mounted)
+        found = probe_output_content (probe_fd);
+
+      if (mounted || found)
+        {
+          if (mounted)
+            fprintf (stderr, _("dd: %s is in use\n"), quotef (file));
+          else
+            fprintf (stderr, _("dd: %s contains %s\n"),
+                     quotef (file), found);
+          fputs (_("This operation may damage the device.\n"),
+                 stderr);
+          if (! confirm_output_overwrite ())
+            exit (EXIT_FAILURE);
+        }
+    }
+
+  iclose (probe_fd);
+}
+
+#else /* !HAVE_BLKID */
+
+/* Without libblkid, the output-device safety check is not available
+   and dd proceeds as if nothing had been detected.  */
+static void
+warn_if_output_in_use (char const *file)
+{
+  (void) file;
+}
+
+#endif /* HAVE_BLKID */
+
 int
 main (int argc, char **argv)
 {
@@ -2474,6 +2684,12 @@ main (int argc, char **argv)
   input_offset = MAX (0, offset);
   input_seek_errno = errno;
 
+  /* If the output is a block device whose contents this run may
+     destroy, ask for confirmation before opening it.  */
+  if (output_file && (max_records || max_bytes)
+      && isatty (STDERR_FILENO))
+    warn_if_output_in_use (output_file);
+
   if (output_file == NULL)
     {
       output_file = _("standard output");
diff --git a/src/local.mk b/src/local.mk
index 6ed4ed78f..889ce50f8 100644
--- a/src/local.mk
+++ b/src/local.mk
@@ -133,7 +133,7 @@ endif
 src_csplit_LDADD = $(LDADD)
 src_cut_LDADD = $(LDADD)
 src_date_LDADD = $(LDADD)
-src_dd_LDADD = $(LDADD)
+src_dd_LDADD = $(LDADD) $(BLKID_LIBS)
 src_df_LDADD = $(LDADD)
 # See dir_LDADD below
 src_dircolors_LDADD = $(LDADD)
-- 
2.25.1

