eq_mode (), which is used to determine if tcsetattr () was successful,
failed unnecessarily after speed setting under certain circumstances:

   1. the kernel MAY round the speed to the nearest supported
      value, so allow a variation of +/- 1/4 bit time per character from
      what was originally requested.

   2. There MAY be more than one internal binary encoding of a specific
      speed (e.g. on Linux.) On systems where that information is
      affects c_cflag, this may introduce inconsistencies; filter out
      the known speed-related flags in c_cflag.

   3. It is undefined in POSIX if cfsetispeed (..., 0) is retained or
      normalized across tcsetattr () ... tcgetattr (). */ If
      cfgetispeed () returns 0, compare the output speed instead.

Signed-off-by: H. Peter Anvin <[email protected]>
---
 src/stty.c | 89 ++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 77 insertions(+), 12 deletions(-)

diff --git a/src/stty.c b/src/stty.c
index d2de216980f7..26432b74e374 100644
--- a/src/stty.c
+++ b/src/stty.c
@@ -439,7 +439,7 @@ static bool recover_mode (char const *arg, struct termios 
*mode);
 static int screen_columns (void);
 static bool set_mode (struct mode_info const *info, bool reversed,
                       struct termios *mode);
-static bool eq_mode (struct termios *mode1, struct termios *mode2);
+static bool eq_mode (const struct termios *mode1, const struct termios *mode2);
 static uintmax_t integer_arg (char const *s, uintmax_t max);
 static speed_t string_to_baud (char const *arg);
 static tcflag_t *mode_type_flag (enum mode_type type, struct termios *mode);
@@ -1436,7 +1436,7 @@ main (int argc, char **argv)
          can report 'success' when it has actually failed to perform
          some proper subset of the requested operations.  To detect
          this partial failure, get the current terminal attributes and
-         compare them to the requested ones.  */
+         compare them to the requested ones. */
 
       if (tcgetattr (STDIN_FILENO, &new_mode))
         error (EXIT_FAILURE, errno, "%s", quotef (device_name));
@@ -1464,21 +1464,86 @@ main (int argc, char **argv)
   return EXIT_SUCCESS;
 }
 
-/* Return true if modes are equivalent.  */
+/* Return true if modes are equivalent.
+
+   However, the speed detection is a bit finicky:
+
+   1. the kernel MAY round the speed to the nearest supported
+      value, so allow a variation of +/- 1/4 bit time per character from
+      what was originally requested.
+
+   2. There MAY be more than one internal binary encoding of a specific
+      speed (e.g. on Linux.) On systems where that information is
+      affects c_cflag, this may introduce inconsistencies.
+
+   3. It is undefined in POSIX if cfsetispeed (..., 0) is retained or
+      normalized across tcsetattr () ... tcgetattr (). */
 
 static bool
-eq_mode (struct termios *mode1, struct termios *mode2)
+eq_mode (const struct termios *mode1, const struct termios *mode2)
 {
-  return mode1->c_iflag == mode2->c_iflag
-      && mode1->c_oflag == mode2->c_oflag
-      && mode1->c_cflag == mode2->c_cflag
-      && mode1->c_lflag == mode2->c_lflag
+  const struct termios * const mode[2] = { mode1, mode2 };
+  unsigned long speed[2][2];
+  tcflag_t cflag_mask;
+  unsigned int bits;
+  int i;
+
+  if (mode1->c_oflag != mode2->c_oflag
+      || mode1->c_lflag != mode2->c_lflag
 #ifdef HAVE_C_LINE
-      && mode1->c_line == mode2->c_line
+      || mode1->c_line != mode2->c_line
 #endif
-      && memeq (mode1->c_cc, mode2->c_cc, sizeof (mode1->c_cc))
-      && cfgetispeed (mode1) == cfgetispeed (mode2)
-      && cfgetospeed (mode1) == cfgetospeed (mode2);
+      || !memeq (mode1->c_cc, mode2->c_cc, sizeof (mode1->c_cc)))
+    return false;
+
+  /* Strip known baud-related flags from c_cflag before comparison */
+  cflag_mask = ~(tcflag_t)0;
+#ifdef CBAUD
+  cflag_mask &= ~CBAUD;
+#endif
+#ifdef CIBAUD
+  cflag_mask &= ~CIBAUD;
+#endif
+
+  if ((mode1->c_cflag ^ mode2->c_cflag) & cflag_mask)
+    return false;
+
+  bits = (mode1->c_cflag & CSTOPB) ? 3 : 2; /* Start and stop bits */
+  switch (mode1->c_cflag & CSIZE) {
+  case CS5:
+    bits += 5;
+    break;
+  case CS6:
+    bits += 6;
+    break;
+  case CS7:
+    bits += 7;
+    break;
+  default:
+    bits += 8;
+    break;
+  }
+
+  for (i = 0; i < 2; i++)
+    {
+      speed_t ospeed = cfgetospeed (mode[i]);
+      speed_t ispeed = cfgetispeed (mode[i]);
+      speed[i][0] = baud_to_value (ospeed);
+      speed[i][1] = baud_to_value (ispeed ? ispeed : ospeed);
+    }
+
+  for (i = 0; i < 2; i++)
+    {
+      unsigned long s1 = speed[0][i];
+      unsigned long s2 = speed[1][i];
+      unsigned long delta = (s1 < s2) ? s2 - s1 : s1 - s2;
+
+      /* s1 is assumed to be the *desired* value, s2 the *actual* value */
+      if (delta > s1/(bits * 4))
+       return false;
+    }
+
+  return true;
 }
 
 /* Return false if not applied because not reversible; otherwise
-- 
2.55.0


Reply via email to