Index | Thread | Search

From:
Damien Miller <djm@mindrot.org>
Subject:
Re: openssh: fractional-second PerSourcePenalties part 1
To:
Theo de Raadt <deraadt@openbsd.org>
Cc:
tech@openbsd.org, openssh@openssh.com, Darren Tucker <dtucker@dtucker.net>
Date:
Tue, 2 Dec 2025 14:51:18 +1100

Download raw body.

Thread
On Mon, 1 Dec 2025, Theo de Raadt wrote:

> > index 20eb305..b2276bb 100644
> > --- a/misc.c
> > +++ b/misc.c
> > @@ -619,6 +619,9 @@ convtime_usec(const char *s)
> >  		errno = 0;
> >  		if ((val = strtod(p, &endp)) < 0 || errno != 0 || p == endp)
> >  			return -1;
> > +		/* Allow only decimal forms */
> > +		if (p + strspn(p, "0123456789.") != endp)
> > +			return -1;
> >  		start_p = p;
> >  		p = endp;
> 
> That looks like a reasonable way of catching the problem.  It removes the
> ability to exercise -, +, NaN Infinity, ( ).  It assumes the whitespace is
> earlier.  It pins the trailing multiplier character.

finally (?), Darren asked for a version that skips the usecs entirely
and passes intervals as double throughout. This turns out to be a bit
simpler again.

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	2 Dec 2025 03:49:58 -0000
@@ -576,25 +576,22 @@ a2tun(const char *s, int *remote)
 	return (tun);
 }
 
-#define SECONDS		1
+#define SECONDS		1.0
 #define MINUTES		(SECONDS * 60)
 #define HOURS		(MINUTES * 60)
 #define DAYS		(HOURS * 24)
 #define WEEKS		(DAYS * 7)
 
-static char *
-scandigits(char *s)
-{
-	while (isdigit((unsigned char)*s))
-		s++;
-	return s;
-}
-
 /*
- * Convert a time string into seconds; format is
- * a sequence of:
+ * Convert an interval/duration time string into seconds, which may include
+ * fractional seconds.
+ *
+ * The 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,44 +601,46 @@ 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 <0.0 if the time string is invalid.
  */
-int
-convtime(const char *s)
+double
+convtime_double(const char *s)
 {
-	int secs, total = 0, multiplier;
-	char *p, *os, *np, c = 0;
-	const char *errstr;
+	double val, total_sec = 0.0, multiplier;
+	const char *p, *start_p;
+	char *endp;
+	int seen_seconds = 0;
 
 	if (s == NULL || *s == '\0')
-		return -1;
-	p = os = strdup(s);	/* deal with const */
-	if (os == NULL)
-		return -1;
+		return -1.0;
 
-	while (*p) {
-		np = scandigits(p);
-		if (np) {
-			c = *np;
-			*np = '\0';
-		}
-		secs = (int)strtonum(p, 0, INT_MAX, &errstr);
-		if (errstr)
-			goto fail;
-		*np = c;
+	for (p = s; *p != '\0';) {
+		if (!isdigit((unsigned char)*p) && *p != '.')
+			return -1.0;
+
+		errno = 0;
+		if ((val = strtod(p, &endp)) < 0 || errno != 0 || p == endp)
+			return -1.0;
+		/* Allow only decimal forms */
+		if (p + strspn(p, "0123456789.") != endp)
+			return -1.0;
+		start_p = p;
+		p = endp;
 
-		multiplier = 1;
-		switch (c) {
+		switch (*p) {
 		case '\0':
-			np--;	/* back up */
-			break;
+			/* FALLTHROUGH */
 		case 's':
 		case 'S':
+			if (seen_seconds++)
+				return -1.0;
+			multiplier = SECONDS;
 			break;
 		case 'm':
 		case 'M':
@@ -660,23 +659,44 @@ convtime(const char *s)
 			multiplier = WEEKS;
 			break;
 		default:
-			goto fail;
+			return -1.0;
+		}
+
+		/* Special handling if this was a decimal */
+		if (memchr(start_p, '.', endp - start_p) != NULL) {
+			/* Decimal point present */
+			if (multiplier > 1.0)
+				return -1.0; /* No fractionals for non-seconds */
+			/* For seconds, ensure digits follow */
+			if (!isdigit((unsigned char)*(endp - 1)))
+				return -1.0;
 		}
-		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;
+
+		total_sec += val * multiplier;
+
+		if (*p != '\0')
+			p++;
 	}
-	free(os);
-	return total;
-fail:
-	free(os);
-	return -1;
+	return total_sec;
+}
+
+/*
+ * Same as convtime_double() above but fractional seconds are ignored.
+ * Return -1 if time string is invalid.
+ */
+int
+convtime(const char *s)
+{
+	double sec_val;
+
+	if ((sec_val = convtime_double(s)) < 0.0)
+		return -1;
+
+	/* Check for overflow into int */
+	if (sec_val < 0 || sec_val > INT_MAX)
+		return -1;
+
+	return (int)sec_val;
 }
 
 #define TF_BUFS	8
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	2 Dec 2025 03:49:58 -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 *);
+double	 convtime_double(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);
Index: usr.bin/ssh/servconf.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/servconf.c,v
diff -u -p -r1.437 servconf.c
--- usr.bin/ssh/servconf.c	25 Nov 2025 00:52:00 -0000	1.437
+++ usr.bin/ssh/servconf.c	2 Dec 2025 03:49:58 -0000
@@ -152,13 +152,13 @@ initialize_server_options(ServerOptions 
 	options->per_source_penalty.max_sources6 = -1;
 	options->per_source_penalty.overflow_mode = -1;
 	options->per_source_penalty.overflow_mode6 = -1;
-	options->per_source_penalty.penalty_crash = -1;
-	options->per_source_penalty.penalty_authfail = -1;
-	options->per_source_penalty.penalty_noauth = -1;
-	options->per_source_penalty.penalty_grace = -1;
-	options->per_source_penalty.penalty_refuseconnection = -1;
-	options->per_source_penalty.penalty_max = -1;
-	options->per_source_penalty.penalty_min = -1;
+	options->per_source_penalty.penalty_crash = -1.0;
+	options->per_source_penalty.penalty_authfail = -1.0;
+	options->per_source_penalty.penalty_noauth = -1.0;
+	options->per_source_penalty.penalty_grace = -1.0;
+	options->per_source_penalty.penalty_refuseconnection = -1.0;
+	options->per_source_penalty.penalty_max = -1.0;
+	options->per_source_penalty.penalty_min = -1.0;
 	options->max_authtries = -1;
 	options->max_sessions = -1;
 	options->banner = NULL;
@@ -399,20 +399,20 @@ fill_default_server_options(ServerOption
 		options->per_source_penalty.overflow_mode = PER_SOURCE_PENALTY_OVERFLOW_PERMISSIVE;
 	if (options->per_source_penalty.overflow_mode6 == -1)
 		options->per_source_penalty.overflow_mode6 = options->per_source_penalty.overflow_mode;
-	if (options->per_source_penalty.penalty_crash == -1)
-		options->per_source_penalty.penalty_crash = 90;
-	if (options->per_source_penalty.penalty_grace == -1)
-		options->per_source_penalty.penalty_grace = 10;
-	if (options->per_source_penalty.penalty_authfail == -1)
-		options->per_source_penalty.penalty_authfail = 5;
-	if (options->per_source_penalty.penalty_noauth == -1)
-		options->per_source_penalty.penalty_noauth = 1;
-	if (options->per_source_penalty.penalty_refuseconnection == -1)
-		options->per_source_penalty.penalty_refuseconnection = 10;
-	if (options->per_source_penalty.penalty_min == -1)
-		options->per_source_penalty.penalty_min = 15;
-	if (options->per_source_penalty.penalty_max == -1)
-		options->per_source_penalty.penalty_max = 600;
+	if (options->per_source_penalty.penalty_crash < 0.0)
+		options->per_source_penalty.penalty_crash = 90.0;
+	if (options->per_source_penalty.penalty_grace < 0.0)
+		options->per_source_penalty.penalty_grace = 10.0;
+	if (options->per_source_penalty.penalty_authfail < 0.0)
+		options->per_source_penalty.penalty_authfail = 5.0;
+	if (options->per_source_penalty.penalty_noauth < 0.0)
+		options->per_source_penalty.penalty_noauth = 1.0;
+	if (options->per_source_penalty.penalty_refuseconnection < 0.0)
+		options->per_source_penalty.penalty_refuseconnection = 10.0;
+	if (options->per_source_penalty.penalty_min < 0.0)
+		options->per_source_penalty.penalty_min = 15.0;
+	if (options->per_source_penalty.penalty_max < 0.0)
+		options->per_source_penalty.penalty_max = 600.0;
 	if (options->max_authtries == -1)
 		options->max_authtries = DEFAULT_AUTH_FAIL_MAX;
 	if (options->max_sessions == -1)
@@ -1257,6 +1257,7 @@ process_server_config_line_depth(ServerO
 {
 	char *str, ***chararrayptr, **charptr, *arg, *arg2, *p, *keyword;
 	int cmdline = 0, *intptr, value, value2, value3, n, port, oactive, r;
+	double dvalue, *doubleptr = NULL;
 	int ca_only = 0, found = 0;
 	SyslogFacility *log_facility_ptr;
 	LogLevel *log_level_ptr;
@@ -2011,7 +2012,8 @@ process_server_config_line_depth(ServerO
 			const char *q = NULL;
 
 			found = 1;
-			value = -1;
+			intptr = NULL;
+			doubleptr = NULL;
 			value2 = 0;
 			/* Allow no/yes only in first position */
 			if (strcasecmp(arg, "no") == 0 ||
@@ -2026,19 +2028,19 @@ process_server_config_line_depth(ServerO
 					options->per_source_penalty.enabled = value2;
 				continue;
 			} else if ((q = strprefix(arg, "crash:", 0)) != NULL) {
-				intptr = &options->per_source_penalty.penalty_crash;
+				doubleptr = &options->per_source_penalty.penalty_crash;
 			} else if ((q = strprefix(arg, "authfail:", 0)) != NULL) {
-				intptr = &options->per_source_penalty.penalty_authfail;
+				doubleptr = &options->per_source_penalty.penalty_authfail;
 			} else if ((q = strprefix(arg, "noauth:", 0)) != NULL) {
-				intptr = &options->per_source_penalty.penalty_noauth;
+				doubleptr = &options->per_source_penalty.penalty_noauth;
 			} else if ((q = strprefix(arg, "grace-exceeded:", 0)) != NULL) {
-				intptr = &options->per_source_penalty.penalty_grace;
+				doubleptr = &options->per_source_penalty.penalty_grace;
 			} else if ((q = strprefix(arg, "refuseconnection:", 0)) != NULL) {
-				intptr = &options->per_source_penalty.penalty_refuseconnection;
+				doubleptr = &options->per_source_penalty.penalty_refuseconnection;
 			} else if ((q = strprefix(arg, "max:", 0)) != NULL) {
-				intptr = &options->per_source_penalty.penalty_max;
+				doubleptr = &options->per_source_penalty.penalty_max;
 			} else if ((q = strprefix(arg, "min:", 0)) != NULL) {
-				intptr = &options->per_source_penalty.penalty_min;
+				doubleptr = &options->per_source_penalty.penalty_min;
 			} else if ((q = strprefix(arg, "max-sources4:", 0)) != NULL) {
 				intptr = &options->per_source_penalty.max_sources4;
 				if ((errstr = atoi_err(q, &value)) != NULL)
@@ -2065,15 +2067,24 @@ process_server_config_line_depth(ServerO
 				fatal("%s line %d: unsupported %s keyword %s",
 				    filename, linenum, keyword, arg);
 			}
-			/* If no value was parsed above, assume it's a time */
-			if (value == -1 && (value = convtime(q)) == -1) {
-				fatal("%s line %d: invalid %s time value.",
-				    filename, linenum, keyword);
-			}
-			if (*activep && *intptr == -1) {
-				*intptr = value;
-				/* any option implicitly enables penalties */
-				options->per_source_penalty.enabled = 1;
+
+			if (doubleptr != NULL) {
+				if ((dvalue = convtime_double(q)) < 0) {
+					fatal("%s line %d: invalid %s time value.",
+					    filename, linenum, keyword);
+				}
+				if (*activep && *doubleptr < 0.0) {
+					*doubleptr = dvalue;
+					options->per_source_penalty.enabled = 1;
+				}
+			} else if (intptr != NULL) {
+				if (*activep && *intptr == -1) {
+					*intptr = value;
+					options->per_source_penalty.enabled = 1;
+				}
+			} else {
+				fatal_f("%s line %d: internal error",
+				    filename, linenum);
 			}
 		}
 		if (!found) {
@@ -3319,8 +3330,8 @@ dump_config(ServerOptions *o)
 	printf("\n");
 
 	if (o->per_source_penalty.enabled) {
-		printf("persourcepenalties crash:%d authfail:%d noauth:%d "
-		    "grace-exceeded:%d refuseconnection:%d max:%d min:%d "
+		printf("persourcepenalties crash:%f authfail:%f noauth:%f "
+		    "grace-exceeded:%f refuseconnection:%f max:%f min:%f "
 		    "max-sources4:%d max-sources6:%d "
 		    "overflow:%s overflow6:%s\n",
 		    o->per_source_penalty.penalty_crash,
Index: usr.bin/ssh/servconf.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/servconf.h,v
diff -u -p -r1.169 servconf.h
--- usr.bin/ssh/servconf.h	14 Oct 2024 01:57:50 -0000	1.169
+++ usr.bin/ssh/servconf.h	2 Dec 2025 03:49:58 -0000
@@ -73,13 +73,13 @@ struct per_source_penalty {
 	int	max_sources6;
 	int	overflow_mode;
 	int	overflow_mode6;
-	int	penalty_crash;
-	int	penalty_grace;
-	int	penalty_authfail;
-	int	penalty_noauth;
-	int	penalty_refuseconnection;
-	int	penalty_max;
-	int	penalty_min;
+	double	penalty_crash;
+	double	penalty_grace;
+	double	penalty_authfail;
+	double	penalty_noauth;
+	double	penalty_refuseconnection;
+	double	penalty_max;
+	double	penalty_min;
 };
 
 typedef struct {
Index: usr.bin/ssh/srclimit.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/srclimit.c,v
diff -u -p -r1.13 srclimit.c
--- usr.bin/ssh/srclimit.c	19 Sep 2025 01:32:45 -0000	1.13
+++ usr.bin/ssh/srclimit.c	2 Dec 2025 03:49:58 -0000
@@ -346,7 +346,8 @@ srclimit_penalise(struct xaddr *addr, in
 	struct xaddr masked;
 	struct penalty *penalty = NULL, *existing = NULL;
 	time_t now;
-	int bits, penalty_secs, max_sources = 0, overflow_mode;
+	int bits, max_sources = 0, overflow_mode;
+	double penalty_secs;
 	char addrnetmask[NI_MAXHOST + 4];
 	const char *reason = NULL, *t;
 	size_t *npenaltiesp = NULL;
@@ -427,7 +428,7 @@ srclimit_penalise(struct xaddr *addr, in
 			fatal_f("internal error: %s penalty tables corrupt", t);
 		do_log2_f(penalty->active ?
 		    SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_VERBOSE,
-		    "%s: new %s %s penalty of %d seconds for %s", t,
+		    "%s: new %s %s penalty of %f seconds for %s", t,
 		    addrnetmask, penalty->active ? "active" : "deferred",
 		    penalty_secs, reason);
 		if (++(*npenaltiesp) > (size_t)max_sources)
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	2 Dec 2025 03:49:58 -0000
@@ -55,6 +55,43 @@ test_convtime(void)
 #endif
 	TEST_DONE();
 
+	TEST_START("misc_convtime_double");
+	ASSERT_DOUBLE_EQ(convtime_double("0"), 0);
+	ASSERT_DOUBLE_EQ(convtime_double("1"), 1.0);
+	ASSERT_DOUBLE_EQ(convtime_double("2s"), 2.0);
+	ASSERT_DOUBLE_EQ(convtime_double("3m"), 180.0);
+	ASSERT_DOUBLE_EQ(convtime_double("1m30s"), 90.0);
+	ASSERT_DOUBLE_EQ(convtime_double("1.5s"), 1.5);
+	ASSERT_DOUBLE_EQ(convtime_double(".5s"), 0.5);
+	ASSERT_DOUBLE_EQ(convtime_double("0.5s"), 0.5);
+	ASSERT_DOUBLE_EQ(convtime_double("1.123456s"), 1.123456);
+	ASSERT_DOUBLE_EQ(convtime_double("1.1234567s"), 1.1234567);
+	ASSERT_DOUBLE_EQ(convtime_double("1.123s"), 1.123);
+	ASSERT_DOUBLE_EQ(convtime_double("1m0.5s"), 60.5);
+	ASSERT_DOUBLE_EQ(convtime_double("1m.5s"), 60.5);
+	ASSERT_DOUBLE_EQ(convtime_double("1.5"), 1.5);
+	ASSERT_DOUBLE_EQ(convtime_double("1.123456"), 1.123456);
+	ASSERT_DOUBLE_EQ(convtime_double("1.1234567"), 1.1234567);
+	/* errors */
+	ASSERT_DOUBLE_LT(convtime_double(""), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("trout"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1.s"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("0x1"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("inf"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("nan"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1e10"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("-1"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("3.w0.5s"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1.0d0.5s"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1.5m"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1.5h"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1.5d"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1.5w"), 0.0);
+	ASSERT_DOUBLE_LT(convtime_double("1s1.5"), 0.0);
+	snprintf(buf, sizeof(buf), "%llds", (long long)(INT64_MAX / 1000000) + 1);
+	ASSERT_DOUBLE_NE(convtime_double(buf), -1.0);
+	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");
Index: regress/usr.bin/ssh/unittests/test_helper/test_helper.c
===================================================================
RCS file: /cvs/src/regress/usr.bin/ssh/unittests/test_helper/test_helper.c,v
diff -u -p -r1.14 test_helper.c
--- regress/usr.bin/ssh/unittests/test_helper/test_helper.c	15 Apr 2025 04:00:42 -0000	1.14
+++ regress/usr.bin/ssh/unittests/test_helper/test_helper.c	2 Dec 2025 03:49:58 -0000
@@ -569,6 +569,47 @@ assert_u64(const char *file, int line, c
 }
 
 void
+assert_double(const char *file, int line, const char *a1, const char *a2,
+    double aa1, double aa2, enum test_predicate pred)
+{
+	const double epsilon = 0.000000001;
+
+	switch (pred) {
+	case TEST_EQ:
+		if (fabs(aa1 - aa2) < epsilon)
+			return;
+		break;
+	case TEST_NE:
+		if (fabs(aa1 - aa2) >= epsilon)
+			return;
+		break;
+	case TEST_LT:
+		if (aa1 < aa2)
+			return;
+		break;
+	case TEST_LE:
+		if (aa1 <= aa2)
+			return;
+		break;
+	case TEST_GT:
+		if (aa1 > aa2)
+			return;
+		break;
+	case TEST_GE:
+		if (aa1 >= aa2)
+			return;
+		break;
+	default:
+		abort();
+	}
+
+	test_header(file, line, a1, a2, "DOUBLE", pred);
+	fprintf(stderr, "%12s = %f\n", a1, aa1);
+	fprintf(stderr, "%12s = %f\n", a2, aa2);
+	test_die();
+}
+
+void
 assert_ptr(const char *file, int line, const char *a1, const char *a2,
     const void *aa1, const void *aa2, enum test_predicate pred)
 {
Index: regress/usr.bin/ssh/unittests/test_helper/test_helper.h
===================================================================
RCS file: /cvs/src/regress/usr.bin/ssh/unittests/test_helper/test_helper.h,v
diff -u -p -r1.10 test_helper.h
--- regress/usr.bin/ssh/unittests/test_helper/test_helper.h	15 Apr 2025 04:00:42 -0000	1.10
+++ regress/usr.bin/ssh/unittests/test_helper/test_helper.h	2 Dec 2025 03:49:58 -0000
@@ -93,6 +93,9 @@ void assert_u32(const char *file, int li
 void assert_u64(const char *file, int line,
     const char *a1, const char *a2,
     u_int64_t aa1, u_int64_t aa2, enum test_predicate pred);
+void assert_double(const char *file, int line,
+    const char *a1, const char *a2,
+    double aa1, double aa2, enum test_predicate pred);
 
 #define TEST_START(n)			test_start(n)
 #define TEST_DONE()			test_done()
@@ -278,6 +281,19 @@ void assert_u64(const char *file, int li
 	assert_u32(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_GE)
 #define ASSERT_U64_GE(a1, a2) \
 	assert_u64(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_GE)
+
+#define ASSERT_DOUBLE_EQ(a1, a2) \
+	assert_double(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_EQ)
+#define ASSERT_DOUBLE_NE(a1, a2) \
+	assert_double(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_NE)
+#define ASSERT_DOUBLE_LT(a1, a2) \
+	assert_double(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_LT)
+#define ASSERT_DOUBLE_LE(a1, a2) \
+	assert_double(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_LE)
+#define ASSERT_DOUBLE_GT(a1, a2) \
+	assert_double(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_GT)
+#define ASSERT_DOUBLE_GE(a1, a2) \
+	assert_double(__FILE__, __LINE__, #a1, #a2, a1, a2, TEST_GE)
 
 /* Benchmarking support */
 #define BENCH_START(name)	\