Index | Thread | Search

From:
Damien Miller <djm@mindrot.org>
Subject:
openssh: fractional-second PerSourcePenalties part 1
To:
tech@openbsd.org
Cc:
openssh@openssh.com
Date:
Fri, 28 Nov 2025 16:20:21 +1100

Download raw body.

Thread
Hi,

I'd like to add fractional second penalties to PerSourcePenalties,
and this is part 1:

We use mis.c:convtime() to parse intervals and durations like
"1h30m15s" and so forth. This is used to parse PerSourcePenalties
times.

To support fractional second penalties, this adds convtime_usec()
that allows things like "1h30m15.123s".

It only allows fractions in the seconds qualifier, i.e. "0.5w" is
disallowed. Obvious junk like "1.5s123" is also disallowed.

This also rewrites the existing convtime() function to use
convtime_usec() internally since they're so similar and the
latter is more general. I got rid of the MINUTES/HOURS/etc defines
because they are pretty obvious.

ok?

Index: usr.bin/ssh/misc.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/misc.c,v
diff -u -p -r1.209 misc.c
--- usr.bin/ssh/misc.c	6 Nov 2025 01:31:11 -0000	1.209
+++ usr.bin/ssh/misc.c	28 Nov 2025 05:07:17 -0000
@@ -576,12 +576,6 @@ a2tun(const char *s, int *remote)
 	return (tun);
 }
 
-#define SECONDS		1
-#define MINUTES		(SECONDS * 60)
-#define HOURS		(MINUTES * 60)
-#define DAYS		(HOURS * 24)
-#define WEEKS		(DAYS * 7)
-
 static char *
 scandigits(char *s)
 {
@@ -591,10 +585,84 @@ scandigits(char *s)
 }
 
 /*
- * Convert a time string into seconds; format is
- * a sequence of:
+ * Parses a decimal number from the start of a string, returning the
+ * value in microseconds.
+ * Returns parsed value on success, -1 on error.
+ * On success, *sp is advanced to the character after the parsed number.
+ */
+static int64_t
+parse_decimal_usec(char **sp)
+{
+	char *p = *sp, *np;
+	const char *errstr;
+	int64_t result_usec = 0, val_int, val_frac = 0;
+	size_t frac_len;
+	char c;
+
+	np = scandigits(p);
+	/* No integer part, but maybe a fraction starting with '.' */
+	if (np == p) {
+		val_int = 0;
+	} else {
+		c = *np;
+		*np = '\0';
+		val_int = strtonum(p, 0, INT64_MAX/1000000, &errstr);
+		*np = c;
+		if (errstr)
+			return -1;
+	}
+	p = np;
+
+	/* Check for fractional part */
+	if (*p == '.') {
+		p++;
+		np = scandigits(p);
+		if (np == p)
+			return -1; /* e.g. "1.s" or "1." */
+		c = *np;
+		*np = '\0';
+		frac_len = strlen(p);
+		if (frac_len > 6) {
+			p[6] = '\0';
+			frac_len = 6;
+		}
+		val_frac = strtonum(p, 0, 999999, &errstr);
+		*np = c;
+		if (errstr)
+			return -1;
+
+		while (frac_len < 6) {
+			val_frac *= 10;
+			frac_len++;
+		}
+		p = np;
+	}
+
+	if (val_int > INT64_MAX / 1000000)
+		return -1;
+	val_int *= 1000000;
+	if (val_int < 0 || val_int > INT64_MAX - val_frac)
+		return -1;
+
+	result_usec = val_int + val_frac;
+
+	*sp = p;
+	return result_usec;
+}
+
+#define SECONDS		1
+#define MINUTES		(SECONDS * 60)
+#define HOURS		(MINUTES * 60)
+#define DAYS		(HOURS * 24)
+#define WEEKS		(DAYS * 7)
+
+/*
+ * Convert a time string into microseconds; format is a sequence of:
  *      time[qualifier]
  *
+ * This supports fractional values for the seconds value only. All other
+ * values must be integers.
+ *
  * Valid time qualifiers are:
  *      <none>  seconds
  *      s|S     seconds
@@ -604,81 +672,109 @@ scandigits(char *s)
  *      w|W     weeks
  *
  * Examples:
- *      90m     90 minutes
- *      1h30m   90 minutes
- *      2d      2 days
- *      1w      1 week
+ *      90m      90 minutes
+ *      1h30m    90 minutes
+ *      1.5s     1.5 seconds
+ *      2d       2 days
+ *      1w       1 week
  *
- * Return -1 if time string is invalid.
+ * Returns -1 if the time string is invalid.
  */
-int
-convtime(const char *s)
+int64_t
+convtime_usec(const char *s)
 {
-	int secs, total = 0, multiplier;
-	char *p, *os, *np, c = 0;
-	const char *errstr;
+	int64_t total_usec = 0;
+	char *p, *orig_s;
+	int seen_seconds = 0;
 
 	if (s == NULL || *s == '\0')
 		return -1;
-	p = os = strdup(s);	/* deal with const */
-	if (os == NULL)
+	orig_s = p = xstrdup(s);
+	if (orig_s == NULL)
 		return -1;
 
 	while (*p) {
-		np = scandigits(p);
-		if (np) {
-			c = *np;
-			*np = '\0';
-		}
-		secs = (int)strtonum(p, 0, INT_MAX, &errstr);
-		if (errstr)
+		int64_t val_usec, multiplier;
+
+		if ((val_usec = parse_decimal_usec(&p)) < 0)
 			goto fail;
-		*np = c;
 
-		multiplier = 1;
-		switch (c) {
+		switch (*p) {
 		case '\0':
-			np--;	/* back up */
-			break;
+			p--; /* so ++p is '\0' */
+			/* FALLTHROUGH */
 		case 's':
 		case 'S':
+			if (seen_seconds++)
+				goto fail;
+			if (total_usec > INT64_MAX - val_usec)
+				goto fail;
+			total_usec += val_usec;
 			break;
 		case 'm':
 		case 'M':
-			multiplier = MINUTES;
-			break;
+			multiplier = 60;
+			goto apply_multiplier;
 		case 'h':
 		case 'H':
-			multiplier = HOURS;
-			break;
+			multiplier = 3600;
+			goto apply_multiplier;
 		case 'd':
 		case 'D':
-			multiplier = DAYS;
-			break;
+			multiplier = 24 * 3600;
+			goto apply_multiplier;
 		case 'w':
 		case 'W':
-			multiplier = WEEKS;
+			multiplier = 7 * 24 * 3600;
+		apply_multiplier:
+			if (val_usec % 1000000 != 0) /* no fractionals for these */
+				goto fail;
+			if (multiplier > 1 && val_usec > INT64_MAX / multiplier)
+				goto fail;
+			val_usec *= multiplier;
+			if (val_usec < 0 || total_usec > INT64_MAX - val_usec)
+				goto fail;
+			total_usec += val_usec;
 			break;
 		default:
 			goto fail;
 		}
-		if (secs > INT_MAX / multiplier)
-			goto fail;
-		secs *= multiplier;
-		if  (total > INT_MAX - secs)
-			goto fail;
-		total += secs;
-		if (total < 0)
-			goto fail;
-		p = ++np;
+		p++;
 	}
-	free(os);
-	return total;
+
+	free(orig_s);
+	return total_usec;
 fail:
-	free(os);
+	free(orig_s);
 	return -1;
 }
 
+/*
+ * Same as convtime_usec() above but fractional seconds are not supported.
+ * Return -1 if time string is invalid.
+ */
+int
+convtime(const char *s)
+{
+	int64_t usec_val;
+
+	if ((usec_val = convtime_usec(s)) == -1)
+		return -1;
+
+	/* Check for fractional seconds: convtime does not support them */
+	if (usec_val % 1000000 != 0)
+		return -1;
+
+	/* Convert to seconds */
+	usec_val /= 1000000;
+
+	/* Check for overflow into int */
+	if (usec_val < 0 || usec_val > INT_MAX)
+		return -1;
+
+	return (int)usec_val;
+}
+
 #define TF_BUFS	8
 #define TF_LEN	9
 
@@ -1744,6 +1840,16 @@ monotime_double(void)
 
 	monotime_ts(&ts);
 	return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0;
+}
+
+/* usec since epoch */
+int64_t
+monotime_usec(void)
+{
+	struct timeval tv;
+
+	monotime_tv(&tv);
+	return (int64_t)tv.tv_sec * 1000000 + tv.tv_usec;
 }
 
 void
Index: usr.bin/ssh/misc.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/misc.h,v
diff -u -p -r1.113 misc.h
--- usr.bin/ssh/misc.h	6 Nov 2025 01:31:11 -0000	1.113
+++ usr.bin/ssh/misc.h	28 Nov 2025 05:07:17 -0000
@@ -82,6 +82,7 @@ int	 parse_user_host_path(const char *, 
 int	 parse_user_host_port(const char *, char **, char **, int *);
 int	 parse_uri(const char *, const char *, char **, char **, int *, char **);
 int	 convtime(const char *);
+int64_t	 convtime_usec(const char *);
 const char *fmt_timeframe(time_t t);
 int	 tilde_expand(const char *, uid_t, char **);
 char	*tilde_expand_filename(const char *, uid_t);
@@ -99,6 +100,7 @@ void	 monotime_ts(struct timespec *);
 void	 monotime_tv(struct timeval *);
 time_t	 monotime(void);
 double	 monotime_double(void);
+int64_t	 monotime_usec(void);
 void	 lowercase(char *s);
 int	 unix_listener(const char *, int, int);
 int	 valid_domain(char *, int, const char **);
Index: regress/usr.bin/ssh/unittests/misc/test_convtime.c
===================================================================
RCS file: /cvs/src/regress/usr.bin/ssh/unittests/misc/test_convtime.c,v
diff -u -p -r1.3 test_convtime.c
--- regress/usr.bin/ssh/unittests/misc/test_convtime.c	11 Aug 2022 01:57:50 -0000	1.3
+++ regress/usr.bin/ssh/unittests/misc/test_convtime.c	28 Nov 2025 05:07:18 -0000
@@ -55,6 +55,37 @@ test_convtime(void)
 #endif
 	TEST_DONE();
 
+	TEST_START("misc_convtime_usec");
+	ASSERT_INT_EQ(convtime_usec("0"), 0);
+	ASSERT_INT_EQ(convtime_usec("1"), 1000000);
+	ASSERT_INT_EQ(convtime_usec("2s"), 2000000);
+	ASSERT_INT_EQ(convtime_usec("3m"), 180000000);
+	ASSERT_INT_EQ(convtime_usec("1m30s"), 90000000);
+	ASSERT_INT_EQ(convtime_usec("1.5s"), 1500000);
+	ASSERT_INT_EQ(convtime_usec(".5s"), 500000);
+	ASSERT_INT_EQ(convtime_usec("0.5s"), 500000);
+	ASSERT_INT_EQ(convtime_usec("1.123456s"), 1123456);
+	ASSERT_INT_EQ(convtime_usec("1.1234567s"), 1123456);
+	ASSERT_INT_EQ(convtime_usec("1.123s"), 1123000);
+	ASSERT_INT_EQ(convtime_usec("1m0.5s"), 60500000);
+	ASSERT_INT_EQ(convtime_usec("1m.5s"), 60500000);
+	ASSERT_INT_EQ(convtime_usec("1.5"), 1500000);
+	ASSERT_INT_EQ(convtime_usec("1.123456"), 1123456);
+	ASSERT_INT_EQ(convtime_usec("1.1234567"), 1123456);
+	/* errors */
+	ASSERT_INT_EQ(convtime_usec(""),  -1);
+	ASSERT_INT_EQ(convtime_usec("trout"),  -1);
+	ASSERT_INT_EQ(convtime_usec("1.s"), -1);
+	ASSERT_INT_EQ(convtime_usec("-1"),  -1);
+	ASSERT_INT_EQ(convtime_usec("1.5m"),  -1);
+	ASSERT_INT_EQ(convtime_usec("1.5h"),  -1);
+	ASSERT_INT_EQ(convtime_usec("1.5d"),  -1);
+	ASSERT_INT_EQ(convtime_usec("1.5w"),  -1);
+	ASSERT_INT_LT(convtime_usec("1s1.5"), 0);
+	snprintf(buf, sizeof(buf), "%llds", (long long)(INT64_MAX / 1000000) + 1);
+	ASSERT_INT_EQ(convtime_usec(buf), -1);
+	TEST_DONE();
+
 	/* XXX timezones/DST make verification of this tricky */
 	/* XXX maybe setenv TZ and tzset() to make it unambiguous? */
 	TEST_START("misc_parse_absolute_time");