Skip to content

Commit f35fe0e

Browse files
committed
sys: timeutil: ensure safety of timespec conversion routines
Originally, the timespec_to_timeout() and timespec_from_timeout() tests assumed that tv_sec and tv_nsec values divided evenly with the number of ticks per second (CONFIG_SYS_CLOCK_TICKS_PER_SEC). However, those assumptions broke down when testing with 32768 ticks/s . Upon further investigation, there were additional corner cases discovered that were not handled in an ideal or safe way. Part of this fix involved identifying several "domains" and special values that have semantic meaning that must be handled particularly carefully. Additional hidden-API constants were made to simplify the solution. K_TICK_MIN (1) K_TICK_MAX (UINT32_MAX-1 in 32-bit, INT64_MAX in 64-bit) K_TS_NO_WAIT (like K_NO_WAIT) K_TS_FOREVER (like K_FOREVER) 1. Converting a negative k_timeout_t These only exist in 64-bit representation and actually encode absolute timepoints via ticks. Since the stated purpose of the conversion functions is to convert between durations, we must reject absolute time points in timespec_from_timeout(). 2. Converting a negative timespec We assume that this duration means a timeout has already expired, and round these up to K_NO_WAIT in timespec_to_timeout(). 3. Due to the larger numeric space in the timespec representation, the reverse mapping of timespec to k_timeout_t is "fuzzy". However, K_NO_WAIT (K_TS_NO_WAIT) and K_FOREVER (K_TS_FOREVER) must remain semantically equivalent. The previous implementation also held this to be true, but the test cases are a bit clearer about it now. 4. Also, due to the larger numeric space in timespec representation, there was a special requirement to round up to the nearest tick boundary for any timespec in the strictly exclusive range (K_TS_NO_WAIT, K_TS_MAX). We must round up, since a) rounding down to K_NO_WAIT is most certainly not representative of a non-zero finite duration delay b) the kernel operates on tick boundaries 5. Above the K_TS_MAX boundary, which is the highest possible timespec that can be represented by a tick, and specifically in the 64-bit timeout representation, there is a domain that cannot be rounded up to K_TS_FOREVER and should always be rounded down to K_TS_MAX. This is to ensure that finite durations remain finite (even if they are beyond the heat death of the universe). Signed-off-by: Chris Friedt <cfriedt@tenstorrent.com>
1 parent fcaf775 commit f35fe0e

File tree

1 file changed

+75
-111
lines changed

1 file changed

+75
-111
lines changed

include/zephyr/sys/timeutil.h

Lines changed: 75 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -383,14 +383,25 @@ static inline bool timespec_normalize(struct timespec *ts)
383383

384384
#if defined(CONFIG_SPEED_OPTIMIZATIONS) && HAS_BUILTIN(__builtin_add_overflow)
385385

386+
int64_t sec = 0;
386387
int sign = (ts->tv_nsec >= 0) - (ts->tv_nsec < 0);
387-
int64_t sec = (ts->tv_nsec >= (long)NSEC_PER_SEC) * (ts->tv_nsec / (long)NSEC_PER_SEC) +
388-
((ts->tv_nsec < 0) && (ts->tv_nsec != LONG_MIN)) *
389-
DIV_ROUND_UP((unsigned long)-ts->tv_nsec, (long)NSEC_PER_SEC) +
390-
(ts->tv_nsec == LONG_MIN) * ((LONG_MAX / NSEC_PER_SEC) + 1);
391-
bool overflow = __builtin_add_overflow(ts->tv_sec, sign * sec, &ts->tv_sec);
392388

393-
ts->tv_nsec -= sign * (long)NSEC_PER_SEC * sec;
389+
/* only one of the following should be non-zero */
390+
sec += (ts->tv_nsec >= (long)NSEC_PER_SEC) * (ts->tv_nsec / (long)NSEC_PER_SEC);
391+
sec += ((sizeof(ts->tv_nsec) != sizeof(int64_t)) && (ts->tv_nsec != LONG_MIN) &&
392+
(ts->tv_nsec < 0)) *
393+
DIV_ROUND_UP((unsigned long)-ts->tv_nsec, (long)NSEC_PER_SEC);
394+
sec += ((sizeof(ts->tv_nsec) == sizeof(int64_t)) && (ts->tv_nsec != INT64_MIN) &&
395+
(ts->tv_nsec < 0)) *
396+
DIV_ROUND_UP((uint64_t)-ts->tv_nsec, NSEC_PER_SEC);
397+
sec += ((sizeof(ts->tv_nsec) != sizeof(int64_t)) && (ts->tv_nsec == LONG_MIN)) *
398+
((LONG_MAX / NSEC_PER_SEC) + 1);
399+
sec += ((sizeof(ts->tv_nsec) == sizeof(int64_t)) && (ts->tv_nsec == INT64_MIN)) *
400+
((INT64_MAX / NSEC_PER_SEC) + 1);
401+
402+
ts->tv_nsec -= sec * sign * NSEC_PER_SEC;
403+
404+
bool overflow = __builtin_add_overflow(ts->tv_sec, sign * sec, &ts->tv_sec);
394405

395406
if (!overflow) {
396407
__ASSERT_NO_MSG(timespec_is_valid(ts));
@@ -405,16 +416,12 @@ static inline bool timespec_normalize(struct timespec *ts)
405416
if (ts->tv_nsec >= (long)NSEC_PER_SEC) {
406417
sec = ts->tv_nsec / (long)NSEC_PER_SEC;
407418
} else if (ts->tv_nsec < 0) {
408-
if ((sizeof(ts->tv_nsec) == sizeof(uint32_t)) && (ts->tv_nsec == LONG_MIN)) {
409-
sec = DIV_ROUND_UP(LONG_MAX / NSEC_PER_USEC, USEC_PER_SEC);
410-
} else {
411-
sec = DIV_ROUND_UP((unsigned long)-ts->tv_nsec, NSEC_PER_SEC);
412-
}
419+
sec = DIV_ROUND_UP((unsigned long)-ts->tv_nsec, NSEC_PER_SEC);
413420
} else {
414421
sec = 0;
415422
}
416423

417-
if ((ts->tv_nsec < 0) && (ts->tv_sec < 0) && (ts->tv_sec - INT64_MIN < sec)) {
424+
if ((ts->tv_nsec < 0) && (ts->tv_sec < 0) && (ts->tv_sec - SYS_TIME_T_MIN < sec)) {
418425
/*
419426
* When `tv_nsec` is negative and `tv_sec` is already most negative,
420427
* further subtraction would cause integer overflow.
@@ -423,7 +430,7 @@ static inline bool timespec_normalize(struct timespec *ts)
423430
}
424431

425432
if ((ts->tv_nsec >= (long)NSEC_PER_SEC) && (ts->tv_sec > 0) &&
426-
(INT64_MAX - ts->tv_sec < sec)) {
433+
(SYS_TIME_T_MAX - ts->tv_sec < sec)) {
427434
/*
428435
* When `tv_nsec` is >= `NSEC_PER_SEC` and `tv_sec` is already most
429436
* positive, further addition would cause integer overflow.
@@ -444,7 +451,6 @@ static inline bool timespec_normalize(struct timespec *ts)
444451
__ASSERT_NO_MSG(timespec_is_valid(ts));
445452

446453
return true;
447-
448454
#endif
449455
}
450456

@@ -476,12 +482,12 @@ static inline bool timespec_add(struct timespec *a, const struct timespec *b)
476482

477483
#else
478484

479-
if ((a->tv_sec < 0) && (b->tv_sec < 0) && (INT64_MIN - a->tv_sec > b->tv_sec)) {
485+
if ((a->tv_sec < 0) && (b->tv_sec < 0) && (SYS_TIME_T_MIN - a->tv_sec > b->tv_sec)) {
480486
/* negative integer overflow would occur */
481487
return false;
482488
}
483489

484-
if ((a->tv_sec > 0) && (b->tv_sec > 0) && (INT64_MAX - a->tv_sec < b->tv_sec)) {
490+
if ((a->tv_sec > 0) && (b->tv_sec > 0) && (SYS_TIME_T_MAX - a->tv_sec < b->tv_sec)) {
485491
/* positive integer overflow would occur */
486492
return false;
487493
}
@@ -517,10 +523,8 @@ static inline bool timespec_negate(struct timespec *ts)
517523

518524
#else
519525

520-
/* note: must check for 32-bit size here until #90029 is resolved */
521-
if (((sizeof(ts->tv_sec) == sizeof(int32_t)) && (ts->tv_sec == INT32_MIN)) ||
522-
((sizeof(ts->tv_sec) == sizeof(int64_t)) && (ts->tv_sec == INT64_MIN))) {
523-
/* -INT64_MIN > INT64_MAX, so +ve integer overflow would occur */
526+
if (ts->tv_sec == SYS_TIME_T_MIN) {
527+
/* -SYS_TIME_T_MIN > SYS_TIME_T_MAX, so positive integer overflow would occur */
524528
return false;
525529
}
526530

@@ -614,123 +618,83 @@ static inline bool timespec_equal(const struct timespec *a, const struct timespe
614618
* This function converts time durations expressed as Zephyr @ref k_timeout_t
615619
* objects to `struct timespec` objects.
616620
*
621+
* @note This function will assert if assertions are enabled and @p timeout is not relative,
622+
* (i.e. a timeout generated by `K_TIMEOUT_ABS_TICKS` or similar is used).
623+
*
617624
* @param timeout the kernel timeout to convert
618625
* @param[out] ts the timespec to store the result
619626
*/
620627
static inline void timespec_from_timeout(k_timeout_t timeout, struct timespec *ts)
621628
{
622629
__ASSERT_NO_MSG(ts != NULL);
623-
624-
#if defined(CONFIG_SPEED_OPTIMIZATIONS)
625-
626-
uint64_t ns = k_ticks_to_ns_ceil64(timeout.ticks);
627-
628-
*ts = (struct timespec){
629-
.tv_sec = (timeout.ticks == K_TICKS_FOREVER) * INT64_MAX +
630-
(timeout.ticks != K_TICKS_FOREVER) * (ns / NSEC_PER_SEC),
631-
.tv_nsec = (timeout.ticks == K_TICKS_FOREVER) * (NSEC_PER_SEC - 1) +
632-
(timeout.ticks != K_TICKS_FOREVER) * (ns % NSEC_PER_SEC),
633-
};
634-
635-
#else
636-
637-
if (timeout.ticks == 0) {
638-
/* This is equivalent to K_NO_WAIT, but without including <zephyr/kernel.h> */
639-
ts->tv_sec = 0;
640-
ts->tv_nsec = 0;
641-
} else if (timeout.ticks == K_TICKS_FOREVER) {
642-
/* This is roughly equivalent to K_FOREVER, but not including <zephyr/kernel.h> */
643-
ts->tv_sec = (time_t)INT64_MAX;
644-
ts->tv_nsec = NSEC_PER_SEC - 1;
630+
__ASSERT_NO_MSG(Z_IS_TIMEOUT_RELATIVE(timeout) ||
631+
(IS_ENABLED(CONFIG_TIMEOUT_64BIT) &&
632+
K_TIMEOUT_EQ(timeout, (k_timeout_t){K_TICKS_FOREVER})));
633+
634+
/* equivalent of K_FOREVER without including kernel.h */
635+
if (K_TIMEOUT_EQ(timeout, (k_timeout_t){K_TICKS_FOREVER})) {
636+
/* duration == K_TICKS_FOREVER ticks */
637+
*ts = K_TS_FOREVER;
638+
/* equivalent of K_NO_WAIT without including kernel.h */
639+
} else if (K_TIMEOUT_EQ(timeout, (k_timeout_t){0})) {
640+
/* duration <= 0 ticks */
641+
*ts = K_TS_NO_WAIT;
645642
} else {
646-
uint64_t ns = k_ticks_to_ns_ceil64(timeout.ticks);
647-
648-
ts->tv_sec = ns / NSEC_PER_SEC;
649-
ts->tv_nsec = ns - ts->tv_sec * NSEC_PER_SEC;
643+
*ts = K_TICKS_TO_TIMESPEC(timeout.ticks);
650644
}
651645

652-
#endif
653-
654646
__ASSERT_NO_MSG(timespec_is_valid(ts));
655647
}
656648

657649
/**
658650
* @brief Convert a timespec to a kernel timeout
659651
*
660-
* This function converts durations expressed as a `struct timespec` to Zephyr @ref k_timeout_t
661-
* objects.
652+
* This function converts a time duration, @p req, expressed as a `timespec` object, to a Zephyr
653+
* @ref k_timeout_t object.
662654
*
663-
* Given that the range of a `struct timespec` is much larger than the range of @ref k_timeout_t,
664-
* and also given that the functions are only intended to be used to convert time durations
665-
* (which are always positive), the function will saturate to @ref K_NO_WAIT if the `tv_sec` field
666-
* of @a ts is negative.
655+
* If @p req contains a negative duration or if both `tv_sec` and `tv_nsec` fields are zero, this
656+
* function will return @ref K_NO_WAIT.
667657
*
668-
* Similarly, if the duration is too large to fit in @ref k_timeout_t, the function will
669-
* saturate to @ref K_FOREVER.
658+
* If @p req contains the maximum representable `timespec`, `{max(time_t), 999999999}`, then this
659+
* function will return @ref K_FOREVER.
670660
*
671-
* @param ts the timespec to convert
672-
* @return the kernel timeout
661+
* If @p req contains a value that is greater than the maximum equivalent tick duration, then this
662+
* function will return the maximum representable tick duration (i.e. @p req will be rounded-down).
663+
*
664+
* Otherwise, this function will return the `k_timeout_t` that is rounded-up to a tick boundary.
665+
*
666+
* @param req the requested `timespec` to convert
667+
* @return the corresponding kernel timeout
673668
*/
674-
static inline k_timeout_t timespec_to_timeout(const struct timespec *ts)
669+
static inline k_timeout_t timespec_to_timeout(const struct timespec *req)
675670
{
676-
__ASSERT_NO_MSG((ts != NULL) && timespec_is_valid(ts));
671+
k_timeout_t timeout;
677672

678-
#if defined(CONFIG_SPEED_OPTIMIZATIONS)
679-
680-
return (k_timeout_t){
681-
/* note: must check for 32-bit size here until #90029 is resolved */
682-
.ticks = ((sizeof(ts->tv_sec) == sizeof(int32_t) && (ts->tv_sec == INT32_MAX) &&
683-
(ts->tv_nsec == NSEC_PER_SEC - 1)) ||
684-
((sizeof(ts->tv_sec) == sizeof(int64_t)) && (ts->tv_sec == INT64_MAX) &&
685-
(ts->tv_nsec == NSEC_PER_SEC - 1))) *
686-
K_TICKS_FOREVER +
687-
((sizeof(ts->tv_sec) == sizeof(int32_t) && (ts->tv_sec == INT32_MAX) &&
688-
(ts->tv_nsec == NSEC_PER_SEC - 1)) ||
689-
((sizeof(ts->tv_sec) == sizeof(int64_t)) && (ts->tv_sec != INT64_MAX) &&
690-
(ts->tv_sec >= 0))) *
691-
(IS_ENABLED(CONFIG_TIMEOUT_64BIT)
692-
? (int64_t)(CLAMP(
693-
k_sec_to_ticks_floor64(ts->tv_sec) +
694-
k_ns_to_ticks_floor64(ts->tv_nsec),
695-
0, (uint64_t)INT64_MAX))
696-
: (uint32_t)(CLAMP(
697-
k_sec_to_ticks_floor64(ts->tv_sec) +
698-
k_ns_to_ticks_floor64(ts->tv_nsec),
699-
0, (uint64_t)UINT32_MAX)))};
673+
__ASSERT_NO_MSG((req != NULL) && timespec_is_valid(req));
700674

701-
#else
675+
if (timespec_compare(req, &K_TS_NO_WAIT) <= 0) {
676+
/* equivalent of K_NO_WAIT without including kernel.h */
677+
timeout.ticks = 0;
678+
return timeout;
679+
}
702680

703-
if ((ts->tv_sec < 0) || (ts->tv_sec == 0 && ts->tv_nsec == 0)) {
704-
/* This is equivalent to K_NO_WAIT, but without including <zephyr/kernel.h> */
705-
return (k_timeout_t){
706-
.ticks = 0,
707-
};
708-
/* note: must check for 32-bit size here until #90029 is resolved */
709-
} else if (((sizeof(ts->tv_sec) == sizeof(int32_t)) && (ts->tv_sec == INT32_MAX) &&
710-
(ts->tv_nsec == NSEC_PER_SEC - 1)) ||
711-
((sizeof(ts->tv_sec) == sizeof(int64_t)) && (ts->tv_sec == INT64_MAX) &&
712-
(ts->tv_nsec == NSEC_PER_SEC - 1))) {
713-
/* This is equivalent to K_FOREVER, but not including <zephyr/kernel.h> */
714-
return (k_timeout_t){
715-
.ticks = K_TICKS_FOREVER,
716-
};
681+
if (timespec_compare(req, &K_TS_FOREVER) == 0) {
682+
/* equivalent of K_FOREVER without including kernel.h */
683+
timeout.ticks = K_TICKS_FOREVER;
684+
return timeout;
685+
}
686+
687+
if (timespec_compare(req, &K_TS_MAX) >= 0) {
688+
/* round down to align to max ticks */
689+
timeout.ticks = K_TICK_MAX;
717690
} else {
718-
if (IS_ENABLED(CONFIG_TIMEOUT_64BIT)) {
719-
return (k_timeout_t){
720-
.ticks = (int64_t)CLAMP(k_sec_to_ticks_floor64(ts->tv_sec) +
721-
k_ns_to_ticks_floor64(ts->tv_nsec),
722-
0, (uint64_t)INT64_MAX),
723-
};
724-
} else {
725-
return (k_timeout_t){
726-
.ticks = (uint32_t)CLAMP(k_sec_to_ticks_floor64(ts->tv_sec) +
727-
k_ns_to_ticks_floor64(ts->tv_nsec),
728-
0, (uint64_t)UINT32_MAX),
729-
};
730-
}
691+
/* round up to align to next tick boundary */
692+
timeout.ticks = CLAMP(k_ns_to_ticks_ceil64(req->tv_nsec) +
693+
k_sec_to_ticks_ceil64(req->tv_sec),
694+
K_TICK_MIN, K_TICK_MAX);
731695
}
732696

733-
#endif
697+
return timeout;
734698
}
735699

736700
/**

0 commit comments

Comments
 (0)