From cc5ec4cd8f953c9c163a91e188a9120c153fa2ff 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 oflag=check to ask before writing to 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.  With oflag=check, dd warns and asks for
confirmation before opening the output, if the target block device
is mounted, or its contents look like an LVM physical volume or an
MBR/GPT partition table.

The check is not the default, as scripts routinely use dd to
overwrite block devices and nobody may be at the terminal to answer.
Without a controlling terminal to ask on, dd proceeds after
printing the warning, and any failure to probe counts as "nothing
detected", so oflag=check can never make dd fail on its own.

* src/dd.c (output_signatures): New.
(flags): Add "check".
(ask_to_proceed, confirm_output_overwrite, device_is_mounted,
match_output_signature, match_partition_table, print_output_warning):
New functions.
(scanargs): Reject iflag=check; strip oflag=check into the new
check_output variable.
(usage): Document the check flag.
(main): With oflag=check, call confirm_output_overwrite before
opening the output.
* NEWS: Mention the new option.
---
 NEWS     |   9 +++
 src/dd.c | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 237 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 84940fbdb..1fce12546 100644
--- a/NEWS
+++ b/NEWS
@@ -99,6 +99,15 @@ GNU coreutils NEWS                                    -*- outline -*-
 
 ** New Features
 
+  dd now supports 'oflag=check', which checks the output before any byte
+  is written: if the output is a block device that is mounted, or whose
+  contents look like an LVM physical volume or a partition table, dd warns
+  and asks for confirmation.  This helps avoid accidentally destroying a
+  disk, e.g., with a mistaken of= argument.  Without oflag=check, dd
+  behaves as before.  When there is no controlling terminal to ask on,
+  dd proceeds after printing the warning, and any failure to probe counts
+  as "nothing detected", so oflag=check 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..8ee037cfb 100644
--- a/src/dd.c
+++ b/src/dd.c
@@ -203,6 +203,9 @@ static off_t input_offset;
 /* True if a partial read should be diagnosed.  */
 static bool warn_partial_read;
 
+/* Whether oflag=check was given: check the output before writing.  */
+static bool check_output;
+
 /* Records truncated by conv=block. */
 static intmax_t r_truncate = 0;
 
@@ -306,7 +309,10 @@ enum
     O_SKIP_BYTES = FFS_MASK (v4),
     v5 = v4 ^ O_SKIP_BYTES,
 
-    O_SEEK_BYTES = FFS_MASK (v5)
+    O_SEEK_BYTES = FFS_MASK (v5),
+    v6 = v5 ^ O_SEEK_BYTES,
+
+    O_CHECK = FFS_MASK (v6)
   };
 
 /* Ensure that we got something.  */
@@ -315,6 +321,7 @@ static_assert (O_NOCACHE != 0);
 static_assert (O_COUNT_BYTES != 0);
 static_assert (O_SKIP_BYTES != 0);
 static_assert (O_SEEK_BYTES != 0);
+static_assert (O_CHECK != 0);
 
 #define MULTIPLE_BITS_SET(i) (((i) & ((i) - 1)) != 0)
 
@@ -324,6 +331,7 @@ static_assert ( ! MULTIPLE_BITS_SET (O_NOCACHE));
 static_assert ( ! MULTIPLE_BITS_SET (O_COUNT_BYTES));
 static_assert ( ! MULTIPLE_BITS_SET (O_SKIP_BYTES));
 static_assert ( ! MULTIPLE_BITS_SET (O_SEEK_BYTES));
+static_assert ( ! MULTIPLE_BITS_SET (O_CHECK));
 
 /* Flags, for iflag="..." and oflag="...".  */
 static struct symbol_value const flags[] =
@@ -346,6 +354,7 @@ static struct symbol_value const flags[] =
   {"count_bytes", O_COUNT_BYTES},
   {"skip_bytes",  O_SKIP_BYTES},
   {"seek_bytes",  O_SEEK_BYTES},
+  {"check",	  O_CHECK},     /* Ask before writing to a block device.  */
   {"",		0}
 };
 
@@ -612,6 +621,10 @@ Each CONV symbol may be:\n\
 Each FLAG symbol may be:\n\
 \n\
   append    append mode (makes sense only for output; conv=notrunc suggested)\n\
+"), stdout);
+      fputs (_("\
+  check     ask before writing to a block device that is mounted, or that\n\
+            contains a partition table or an LVM physical volume (oflag only)\n\
 "), stdout);
       if (O_CIO)
         fputs (_("  cio       use concurrent I/O for data\n"), stdout);
@@ -1633,6 +1646,12 @@ scanargs (int argc, char *const *argv)
       usage (EXIT_FAILURE);
     }
 
+  if (input_flags & O_CHECK)
+    {
+      diagnose (0, "%s: %s", _("invalid input flag"), quote ("check"));
+      usage (EXIT_FAILURE);
+    }
+
   if (skip_B)
     input_flags |= O_SKIP_BYTES;
   if (input_flags & O_SKIP_BYTES && skip != 0)
@@ -1702,6 +1721,12 @@ scanargs (int argc, char *const *argv)
       o_nocache_eof = (max_records == 0 && max_bytes == 0);
       output_flags &= ~O_NOCACHE;
     }
+
+  if (output_flags & O_CHECK)
+    {
+      check_output = true;
+      output_flags &= ~O_CHECK;
+    }
 }
 
 /* Fix up translation table. */
@@ -2425,6 +2450,202 @@ synchronize_output (void)
   return exit_status;
 }
 
+/* Advisory safety check for the output of dd, enabled with
+   oflag=check:  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_signature
+{
+  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_signature const output_signatures[] =
+{
+  { 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_signatures, or NULL if nothing matches.  */
+static char const *
+match_output_signature (ssize_t probe_got)
+{
+  for (struct output_signature const *p = output_signatures; 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 *
+match_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;
+}
+
+/* Print on FP the warning that the output of dd, FILE, is about to
+   be overwritten, where it is MOUNTED, or where FOUND says what its
+   contents look like.  */
+static void
+print_output_warning (FILE *fp, char const *file, bool mounted,
+                      char const *found)
+{
+  fprintf (fp, "%s: ", program_name);
+  if (mounted)
+    fprintf (fp, _("%s is in use"), quotef (file));
+  else
+    fprintf (fp, _("%s contains %s"), quotef (file), found);
+  fputc ('\n', fp);
+  fputs (_("This operation may damage the device.\n"), fp);
+}
+
+/* Ask on the controlling terminal whether to continue.  Return
+   true for an explicit "y" answer, for an empty line, or for an
+   unavailable terminal, which means the default; any other answer
+   or EOF means no.  If standard error is not a terminal, repeat
+   the warning there first, so that the question is not asked
+   without its context.  Ask on /dev/tty rather than on stdin,
+   which carries the data to copy.  */
+static bool
+ask_to_proceed (char const *file, bool mounted, char const *found)
+{
+  FILE *tty = fopen ("/dev/tty", "r+");
+  if (! tty)
+    return true;
+
+  if (! isatty (STDERR_FILENO))
+    print_output_warning (tty, file, mounted, found);
+
+  fputs (_("Proceed anyway? (Y/n) "), tty);
+  fflush (tty);
+
+  char answer[8];
+  char *s = fgets (answer, sizeof answer, tty);
+  fclose (tty);
+  return s && (s[0] == '\n' || s[0] == 'y' || s[0] == 'Y');
+}
+
+/* Ask for confirmation before overwriting FILE, the output of dd,
+   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
+confirm_output_overwrite (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 = match_output_signature (probe_got);
+              if (! found)
+                found = match_partition_table (probe_got);
+            }
+        }
+
+      if (mounted || found)
+        {
+          print_output_warning (stderr, file, mounted, found);
+          if (! ask_to_proceed (file, mounted, found))
+            exit (EXIT_FAILURE);
+        }
+    }
+
+  iclose (probe_fd);
+}
+
 int
 main (int argc, char **argv)
 {
@@ -2474,6 +2695,12 @@ main (int argc, char **argv)
   input_offset = MAX (0, offset);
   input_seek_errno = errno;
 
+  /* If oflag=check was given and the output is a block device whose
+     contents this run may destroy, ask for confirmation
+     before opening it.  */
+  if (check_output && output_file && (max_records || max_bytes))
+    confirm_output_overwrite (output_file);
+
   if (output_file == NULL)
     {
       output_file = _("standard output");
-- 
2.25.1

