Download raw body.
openssh: fractional-second PerSourcePenalties part 1
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");
openssh: fractional-second PerSourcePenalties part 1