From 388868538533b87e0a7a1de7f275f6ba5468f9cc 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

A mistaken of= argument can make dd destroy data on a block device
in a single command: a mounted file system, an LVM physical volume,
or a partition table.  Before opening the output, dd now warns and
asks for confirmation if the target block device is mounted, or its
contents look like an LVM physical volume or an MBR/GPT partition
table.  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.

* src/dd.c (output_probes): New.
(device_is_mounted, probe_output_content, probe_output_partition_table,
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 check.
---
 NEWS     |   7 +++
 src/dd.c | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 193 insertions(+)

diff --git a/NEWS b/NEWS
index 84940fbdb..0a545bdb4 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 an LVM physical volume or a
+  partition table.  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/src/dd.c b/src/dd.c
index 26382a233..9a769c408 100644
--- a/src/dd.c
+++ b/src/dd.c
@@ -2425,6 +2425,186 @@ synchronize_output (void)
   return exit_status;
 }
 
+/* Advisory safety check for the output of dd:  if the output is a
+   block device that is in use, or whose contents look like 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.  */
+
+/* Signature of data stored directly on a block device, probed at a
+   fixed offset in the device's own byte stream.  */
+struct output_probe
+{
+  char const *name;           /* What to report if the magic matches.  */
+  off_t offset;               /* Offset of the magic from the device start.  */
+  char const *magic;          /* Expected bytes at OFFSET.  */
+};
+
+/* Signatures of LVM physical volumes.  The label is also probed on
+   logical volumes, as LVM supports stacking a new physical volume
+   on top of one.  */
+static struct output_probe const output_probes[] =
+{
+  { N_("an LVM physical volume"), 0, "LABELONE" },
+  { N_("an LVM physical volume"), 512, "LABELONE" },
+  { N_("an LVM physical volume"), 1024, "LABELONE" },
+  { N_("an LVM physical volume"), 1536, "LABELONE" },
+  { NULL, 0, NULL }
+};
+
+/* One read of the device start covers every probe offset; the
+   highest is the GPT header on devices with 4096-byte native
+   sectors, at byte 4096.  */
+static unsigned char probe_buf[5120];
+
+/* Return what the device whose first PROBE_GOT bytes are in
+   probe_buf contains, judging from the signatures in
+   output_probes, or NULL if nothing matches.  */
+static char const *
+probe_output_content (ssize_t probe_got)
+{
+  for (struct output_probe const *p = output_probes; p->name; p++)
+    {
+      idx_t magic_len = strlen (p->magic);
+      if (p->offset + magic_len <= probe_got
+          && memcmp (probe_buf + p->offset, p->magic, magic_len) == 0)
+        return _(p->name);
+    }
+  return NULL;
+}
+
+/* Return what kind of partition table the device whose first
+   PROBE_GOT bytes are in probe_buf contains, or NULL if none
+   is recognized.  For an MBR, require a valid partition entry
+   rather than just the 0x55AA signature, to avoid misjudging a
+   bare boot sector.  */
+static char const *
+probe_output_partition_table (ssize_t probe_got)
+{
+  /* GPT header: "EFI PART" at LBA 1, or at byte 4096 on devices
+     with 4096-byte native sectors.  */
+  if ((512 + 8 <= probe_got
+       && memcmp (probe_buf + 512, "EFI PART", 8) == 0)
+      || (4096 + 8 <= probe_got
+          && memcmp (probe_buf + 4096, "EFI PART", 8) == 0))
+    return _("a GPT partition table");
+
+  if (512 <= probe_got
+      && probe_buf[510] == 0x55 && probe_buf[511] == 0xaa)
+    {
+      for (int i = 0; i < 4; i++)
+        {
+          unsigned char const *e = probe_buf + 446 + 16 * i;
+          if (e[4] != 0 && (e[0] == 0x00 || e[0] == 0x80))
+            return _("an MBR partition table");
+        }
+    }
+  return NULL;
+}
+
+/* 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， for an empty line or an
+   unavailable terminal, which means the default or any other
+   answer, EOF 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 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)
+        {
+          ssize_t probe_got = read (probe_fd, probe_buf, sizeof probe_buf);
+          if (0 < probe_got)
+            {
+              found = probe_output_content (probe_got);
+              if (! found)
+                found = probe_output_partition_table (probe_got);
+            }
+        }
+
+      if (mounted || found)
+        {
+          if (mounted)
+            error (0, 0, _("%s is in use"), quotef (file));
+          else
+            error (0, 0, _("%s contains %s"), quotef (file), found);
+          fputs (_("This operation may damage the device.\n"),
+                 stderr);
+          if (! confirm_output_overwrite ())
+            exit (EXIT_FAILURE);
+        }
+    }
+
+  iclose (probe_fd);
+}
+
 int
 main (int argc, char **argv)
 {
@@ -2474,6 +2654,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");
-- 
2.25.1

