Skip to content

Commit b751fb0

Browse files
committed
Support integer parts-per-second time units with hardened conversion bounds
- Add support for integer parts-per-second time units (e.g. 1_000_000 for microseconds) - Refactor time-unit conversion into shared helper functions - Harden calendar and integer time unit conversion bounds - Fix input validation and negative rounding in calendar:system_time_to_universal_time/2 - Ensure BEAM compatibility for monotonic_time and system_time tests - Use AVM_INT range for calendar year and add year boundary tests Signed-off-by: Peter M <petermm@gmail.com>
1 parent 86ba734 commit b751fb0

6 files changed

Lines changed: 413 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Added support for configuring pins and width for sdmmc on ESP32
2525
- Added support for map comprehensions
2626
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
27+
- Added support for integer parts-per-second time unit, with `badarg` raised on int64 overflow
2728

2829
### Changed
2930
- Updated network type db() to dbm() to reflect the actual representation of the type

libs/estdlib/src/erlang.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@
189189
-type atom_encoding() :: latin1 | utf8 | unicode.
190190

191191
-type mem_type() :: binary.
192-
-type time_unit() :: second | millisecond | microsecond | nanosecond | native.
192+
-type time_unit() :: second | millisecond | microsecond | nanosecond | native | pos_integer().
193193
-type timestamp() :: {
194194
MegaSecs :: non_neg_integer(), Secs :: non_neg_integer(), MicroSecs :: non_neg_integer
195195
}.

libs/exavmlib/lib/System.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ defmodule System do
2626
| :millisecond
2727
| :microsecond
2828
| :nanosecond
29+
| pos_integer()
2930

3031
@doc """
3132
Returns the current monotonic time in the `:native` time unit.

src/libAtomVM/nifs.c

Lines changed: 173 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,70 +1851,184 @@ term nif_erlang_make_ref_0(Context *ctx, int argc, term argv[])
18511851
return term_from_ref_ticks(ref_ticks, &ctx->heap);
18521852
}
18531853

1854-
term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
1854+
static bool time_unit_to_parts_per_second(term unit, avm_int64_t *parts_per_second)
18551855
{
1856-
UNUSED(ctx);
1857-
1858-
term unit;
1859-
if (argc == 0) {
1860-
unit = NATIVE_ATOM;
1856+
if (unit == SECOND_ATOM) {
1857+
*parts_per_second = 1;
1858+
} else if (unit == MILLISECOND_ATOM) {
1859+
*parts_per_second = 1000;
1860+
} else if (unit == MICROSECOND_ATOM) {
1861+
*parts_per_second = INT64_C(1000000);
1862+
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1863+
// AtomVM exposes the Erlang `native` time unit as nanoseconds on all platforms.
1864+
*parts_per_second = INT64_C(1000000000);
1865+
} else if (term_is_int64(unit)) {
1866+
*parts_per_second = term_maybe_unbox_int64(unit);
1867+
if (UNLIKELY(*parts_per_second <= 0)) {
1868+
return false;
1869+
}
18611870
} else {
1862-
unit = argv[0];
1871+
return false;
18631872
}
18641873

1865-
struct timespec ts;
1866-
sys_monotonic_time(&ts);
1874+
return true;
1875+
}
18671876

1868-
if (unit == SECOND_ATOM) {
1869-
return make_maybe_boxed_int64(ctx, ts.tv_sec);
1877+
// Convert nanoseconds to parts using: parts = nanoseconds * pps / 1e9
1878+
// Splits into high/low to avoid intermediate overflow.
1879+
static bool nanoseconds_to_parts_per_second(
1880+
avm_int64_t nanoseconds, avm_int64_t parts_per_second, bool round_up, avm_int64_t *parts)
1881+
{
1882+
if (UNLIKELY(
1883+
nanoseconds < 0 || nanoseconds >= INT64_C(1000000000) || parts_per_second <= 0)) {
1884+
return false;
1885+
}
18701886

1871-
} else if (unit == MILLISECOND_ATOM) {
1872-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
1887+
avm_int64_t quotient = parts_per_second / INT64_C(1000000000);
1888+
avm_int64_t remainder = parts_per_second % INT64_C(1000000000);
1889+
avm_int64_t fractional_high = nanoseconds * quotient;
1890+
avm_int64_t remainder_product = nanoseconds * remainder;
1891+
avm_int64_t fractional_low = remainder_product / INT64_C(1000000000);
18731892

1874-
} else if (unit == MICROSECOND_ATOM) {
1875-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
1893+
if (round_up && (remainder_product % INT64_C(1000000000)) != 0) {
1894+
fractional_low += 1;
1895+
}
18761896

1877-
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1878-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
1897+
if (UNLIKELY(fractional_high > INT64_MAX - fractional_low)) {
1898+
return false;
1899+
}
18791900

1880-
} else {
1881-
RAISE_ERROR(BADARG_ATOM);
1901+
*parts = fractional_high + fractional_low;
1902+
return true;
1903+
}
1904+
1905+
// Convert a normalized timespec (0 <= tv_nsec < 1e9) to integer parts.
1906+
// Uses floor semantics for negative timestamps with non-zero tv_nsec.
1907+
static bool timespec_to_parts_per_second(
1908+
const struct timespec *ts, avm_int64_t parts_per_second, avm_int64_t *parts)
1909+
{
1910+
if (UNLIKELY(
1911+
parts_per_second <= 0 || ts->tv_nsec < 0 || ts->tv_nsec >= INT64_C(1000000000))) {
1912+
return false;
18821913
}
1914+
1915+
avm_int64_t seconds = (avm_int64_t) ts->tv_sec;
1916+
avm_int64_t fractional_part;
1917+
1918+
if (ts->tv_nsec == 0 || seconds >= 0) {
1919+
if (UNLIKELY(
1920+
((seconds > 0) && (seconds > INT64_MAX / parts_per_second))
1921+
|| ((seconds < 0) && (seconds < INT64_MIN / parts_per_second)))) {
1922+
return false;
1923+
}
1924+
1925+
if (UNLIKELY(!nanoseconds_to_parts_per_second(
1926+
(avm_int64_t) ts->tv_nsec, parts_per_second, false, &fractional_part))) {
1927+
return false;
1928+
}
1929+
1930+
avm_int64_t second_part = seconds * parts_per_second;
1931+
if (UNLIKELY(second_part > INT64_MAX - fractional_part)) {
1932+
return false;
1933+
}
1934+
1935+
*parts = second_part + fractional_part;
1936+
return true;
1937+
}
1938+
1939+
// Preserve floor semantics for normalized negative timespecs such as {-2, 999999999}.
1940+
avm_int64_t adjusted_seconds = seconds + 1;
1941+
if (UNLIKELY(adjusted_seconds < INT64_MIN / parts_per_second)) {
1942+
return false;
1943+
}
1944+
1945+
if (UNLIKELY(!nanoseconds_to_parts_per_second(
1946+
INT64_C(1000000000) - (avm_int64_t) ts->tv_nsec, parts_per_second, true,
1947+
&fractional_part))) {
1948+
return false;
1949+
}
1950+
1951+
avm_int64_t second_part = adjusted_seconds * parts_per_second;
1952+
if (UNLIKELY(second_part < INT64_MIN + fractional_part)) {
1953+
return false;
1954+
}
1955+
1956+
*parts = second_part - fractional_part;
1957+
return true;
18831958
}
18841959

1885-
term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
1960+
static term make_time_in_unit(Context *ctx, term unit, void (*time_fun)(struct timespec *))
18861961
{
1887-
UNUSED(ctx);
1962+
avm_int64_t parts_per_second;
1963+
if (UNLIKELY(!time_unit_to_parts_per_second(unit, &parts_per_second))) {
1964+
RAISE_ERROR(BADARG_ATOM);
1965+
}
18881966

1967+
struct timespec ts;
1968+
time_fun(&ts);
1969+
1970+
avm_int64_t value;
1971+
if (UNLIKELY(!timespec_to_parts_per_second(&ts, parts_per_second, &value))) {
1972+
RAISE_ERROR(BADARG_ATOM);
1973+
}
1974+
1975+
return make_maybe_boxed_int64(ctx, value);
1976+
}
1977+
1978+
term nif_erlang_monotonic_time_1(Context *ctx, int argc, term argv[])
1979+
{
18891980
term unit;
18901981
if (argc == 0) {
18911982
unit = NATIVE_ATOM;
18921983
} else {
18931984
unit = argv[0];
18941985
}
18951986

1896-
struct timespec ts;
1897-
sys_time(&ts);
1987+
return make_time_in_unit(ctx, unit, sys_monotonic_time);
1988+
}
18981989

1899-
if (unit == SECOND_ATOM) {
1900-
return make_maybe_boxed_int64(ctx, ts.tv_sec);
1990+
term nif_erlang_system_time_1(Context *ctx, int argc, term argv[])
1991+
{
1992+
term unit;
1993+
if (argc == 0) {
1994+
unit = NATIVE_ATOM;
1995+
} else {
1996+
unit = argv[0];
1997+
}
19011998

1902-
} else if (unit == MILLISECOND_ATOM) {
1903-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000UL + ts.tv_nsec / 1000000UL);
1999+
return make_time_in_unit(ctx, unit, sys_time);
2000+
}
19042001

1905-
} else if (unit == MICROSECOND_ATOM) {
1906-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * 1000000UL + ts.tv_nsec / 1000UL);
2002+
static bool int64_to_time_t_checked(avm_int64_t seconds, time_t *out)
2003+
{
2004+
if (((time_t) -1) > (time_t) 0) {
2005+
if (seconds < 0) {
2006+
return false;
2007+
}
19072008

1908-
} else if (unit == NANOSECOND_ATOM || unit == NATIVE_ATOM) {
1909-
return make_maybe_boxed_int64(ctx, ((int64_t) ts.tv_sec) * INT64_C(1000000000) + ts.tv_nsec);
2009+
time_t time_seconds = (time_t) (uint64_t) seconds;
2010+
if ((uint64_t) time_seconds != (uint64_t) seconds) {
2011+
return false;
2012+
}
2013+
*out = time_seconds;
2014+
return true;
2015+
}
19102016

1911-
} else {
1912-
RAISE_ERROR(BADARG_ATOM);
2017+
time_t time_seconds = (time_t) seconds;
2018+
if ((avm_int64_t) time_seconds != seconds) {
2019+
return false;
19132020
}
2021+
*out = time_seconds;
2022+
return true;
19142023
}
19152024

19162025
static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
19172026
{
2027+
avm_int64_t year = (avm_int64_t) broken_down_time->tm_year + 1900;
2028+
if (UNLIKELY(year < AVM_INT_MIN || year > AVM_INT_MAX)) {
2029+
RAISE_ERROR(BADARG_ATOM);
2030+
}
2031+
19182032
// 4 = size of date/time tuple, 3 size of date time tuple
19192033
if (UNLIKELY(memory_ensure_free_opt(ctx, 3 + 4 + 4, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) {
19202034
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
@@ -1923,7 +2037,7 @@ static term build_datetime_from_tm(Context *ctx, struct tm *broken_down_time)
19232037
term time_tuple = term_alloc_tuple(3, &ctx->heap);
19242038
term date_time_tuple = term_alloc_tuple(2, &ctx->heap);
19252039

1926-
term_put_tuple_element(date_tuple, 0, term_from_int11(1900 + broken_down_time->tm_year));
2040+
term_put_tuple_element(date_tuple, 0, term_from_int((avm_int_t) year));
19272041
term_put_tuple_element(date_tuple, 1, term_from_int11(broken_down_time->tm_mon + 1));
19282042
term_put_tuple_element(date_tuple, 2, term_from_int11(broken_down_time->tm_mday));
19292043

@@ -1946,7 +2060,12 @@ term nif_erlang_universaltime_0(Context *ctx, int argc, term argv[])
19462060
sys_time(&ts);
19472061

19482062
struct tm broken_down_time;
1949-
return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time));
2063+
struct tm *universal_time = gmtime_r(&ts.tv_sec, &broken_down_time);
2064+
if (UNLIKELY(universal_time == NULL)) {
2065+
RAISE_ERROR(BADARG_ATOM);
2066+
}
2067+
2068+
return build_datetime_from_tm(ctx, universal_time);
19502069
}
19512070

19522071
// setenv leaks the prior "TZ=value" string on overwrite (unbounded on
@@ -2095,35 +2214,35 @@ term nif_erlang_timestamp_0(Context *ctx, int argc, term argv[])
20952214

20962215
term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term argv[])
20972216
{
2098-
UNUSED(ctx);
20992217
UNUSED(argc);
21002218

2101-
struct timespec ts;
2102-
2219+
if (UNLIKELY(!term_is_int64(argv[0]))) {
2220+
RAISE_ERROR(BADARG_ATOM);
2221+
}
21032222
avm_int64_t value = term_maybe_unbox_int64(argv[0]);
21042223

2105-
if (argv[1] == SECOND_ATOM) {
2106-
ts.tv_sec = (time_t) value;
2107-
ts.tv_nsec = 0;
2108-
2109-
} else if (argv[1] == MILLISECOND_ATOM) {
2110-
ts.tv_sec = (time_t) (value / 1000);
2111-
ts.tv_nsec = (value % 1000) * 1000000;
2112-
2113-
} else if (argv[1] == MICROSECOND_ATOM) {
2114-
ts.tv_sec = (time_t) (value / 1000000);
2115-
ts.tv_nsec = (value % 1000000) * 1000;
2224+
avm_int64_t parts_per_second;
2225+
if (UNLIKELY(!time_unit_to_parts_per_second(argv[1], &parts_per_second))) {
2226+
RAISE_ERROR(BADARG_ATOM);
2227+
}
21162228

2117-
} else if (argv[1] == NANOSECOND_ATOM || argv[1] == NATIVE_ATOM) {
2118-
ts.tv_sec = (time_t) (value / INT64_C(1000000000));
2119-
ts.tv_nsec = value % INT64_C(1000000000);
2229+
// Floor division: round negative fractional seconds toward negative infinity
2230+
avm_int64_t quotient = value / parts_per_second;
2231+
avm_int64_t remainder = value % parts_per_second;
2232+
avm_int64_t seconds = quotient - (remainder < 0);
21202233

2121-
} else {
2234+
time_t time_seconds;
2235+
if (UNLIKELY(!int64_to_time_t_checked(seconds, &time_seconds))) {
21222236
RAISE_ERROR(BADARG_ATOM);
21232237
}
21242238

21252239
struct tm broken_down_time;
2126-
return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time));
2240+
struct tm *universal_time = gmtime_r(&time_seconds, &broken_down_time);
2241+
if (UNLIKELY(universal_time == NULL)) {
2242+
RAISE_ERROR(BADARG_ATOM);
2243+
}
2244+
2245+
return build_datetime_from_tm(ctx, universal_time);
21272246
}
21282247

21292248
static term nif_os_getenv_1(Context *ctx, int argc, term argv[])

tests/erlang_tests/test_monotonic_time.erl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ start() ->
3838
true = is_integer(N2 - N1) andalso (N2 - N1) >= 0,
3939

4040
ok = test_native_monotonic_time(),
41+
ok = test_integer_time_unit(),
42+
ok = test_non_power_of_10_integer_time_unit(),
43+
ok = test_bad_integer_time_unit(),
4144

4245
1.
4346

@@ -46,6 +49,15 @@ test_diff(X) when is_integer(X) andalso X >= 0 ->
4649
test_diff(X) when X < 0 ->
4750
0.
4851

52+
expect(F, Expect) ->
53+
try
54+
F(),
55+
fail
56+
catch
57+
_:E when E == Expect ->
58+
ok
59+
end.
60+
4961
test_native_monotonic_time() ->
5062
Na1 = erlang:monotonic_time(native),
5163
receive
@@ -54,3 +66,50 @@ test_native_monotonic_time() ->
5466
Na2 = erlang:monotonic_time(native),
5567
true = is_integer(Na2 - Na1) andalso (Na2 - Na1) >= 0,
5668
ok.
69+
70+
test_integer_time_unit() ->
71+
S = erlang:monotonic_time(second),
72+
S1 = erlang:monotonic_time(1),
73+
true = abs(S1 - S) =< 1,
74+
75+
Ms = erlang:monotonic_time(millisecond),
76+
Ms1 = erlang:monotonic_time(1000),
77+
true = abs(Ms1 - Ms) =< 1,
78+
79+
Us = erlang:monotonic_time(microsecond),
80+
Us1 = erlang:monotonic_time(1000000),
81+
true = abs(Us1 - Us) =< 1000,
82+
83+
Ns = erlang:monotonic_time(nanosecond),
84+
Ns1 = erlang:monotonic_time(1000000000),
85+
true = abs(Ns1 - Ns) =< 1000000,
86+
87+
T1 = erlang:monotonic_time(1000),
88+
receive
89+
after 1 -> ok
90+
end,
91+
T2 = erlang:monotonic_time(1000),
92+
true = T2 >= T1,
93+
94+
ok.
95+
96+
test_non_power_of_10_integer_time_unit() ->
97+
ok = test_integer_time_unit_monotonicity(256),
98+
ok = test_integer_time_unit_monotonicity(48000),
99+
ok.
100+
101+
test_bad_integer_time_unit() ->
102+
ok = expect(fun() -> erlang:monotonic_time(0) end, badarg),
103+
ok = expect(fun() -> erlang:monotonic_time(-1) end, badarg),
104+
ok.
105+
106+
test_integer_time_unit_monotonicity(PartsPerSecond) ->
107+
T1 = erlang:monotonic_time(PartsPerSecond),
108+
receive
109+
after 1 -> ok
110+
end,
111+
T2 = erlang:monotonic_time(PartsPerSecond),
112+
true = is_integer(T1),
113+
true = is_integer(T2),
114+
true = T2 >= T1,
115+
ok.

0 commit comments

Comments
 (0)