Skip to content

Commit d3169f4

Browse files
committed
Harden localtime/1 TZ workaround for correctness and portability
Guard the static putenv buffer workaround behind __NEWLIB__/__PICOLIBC__ so platforms with non-leaking libc (glibc, musl) use standard setenv/unsetenv. Picolibc (used by ESP-IDF 6) has the same setenv leak. Additional fixes: - Replace space-padding with zero-fill to avoid TZ parser issues - Reject oversized TZ values instead of silently truncating - Restore "UTC0" instead of empty string when TZ was originally unset - Check strdup for OOM, putenv return value, and localtime_r for NULL Signed-off-by: Peter M <petermm@gmail.com>
1 parent 6e55fef commit d3169f4

1 file changed

Lines changed: 57 additions & 11 deletions

File tree

src/libAtomVM/nifs.c

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,37 +1939,55 @@ term nif_erlang_universaltime_0(Context *ctx, int argc, term argv[])
19391939
return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time));
19401940
}
19411941

1942-
// Workaround for newlib setenv memory leak: use putenv with a fixed-size
1943-
// static buffer. The buffer is installed once and then modified in place.
1942+
// Workaround for newlib/picolibc setenv memory leak: use putenv with a
1943+
// fixed-size static buffer. The buffer is installed once via putenv and then
1944+
// modified in place so repeated TZ changes never allocate.
19441945
// See: https://github.com/espressif/esp-idf/issues/3046
1946+
// Both newlib and picolibc leak the old "NAME=value" string on overwrite.
1947+
#if defined(__NEWLIB__) || defined(__PICOLIBC__)
1948+
#define AVM_TZ_SETENV_LEAKS 1
1949+
#else
1950+
#define AVM_TZ_SETENV_LEAKS 0
1951+
#endif
1952+
1953+
#if AVM_TZ_SETENV_LEAKS
19451954

1955+
// Max TZ value length is 60 bytes; longest POSIX TZ strings (e.g.
1956+
// "CET-1CEST,M3.5.0/2,M10.5.0/3") are well under this limit.
19461957
#define TZ_BUFFER_SIZE 64
1958+
#define TZ_MAX_VALUE_LEN (TZ_BUFFER_SIZE - 4) // 3 for "TZ=" + 1 for '\0'
19471959

19481960
static char tz_buffer[TZ_BUFFER_SIZE] = "TZ=";
19491961
static bool tz_buffer_installed = false;
19501962
static char *tz_env_value = NULL;
19511963

1952-
static void set_tz_value(const char *tz)
1964+
// Write a TZ value into the static buffer. Returns false if the value is
1965+
// too long to fit (the buffer is left unchanged in that case).
1966+
// Caller must hold env_spinlock.
1967+
static bool set_tz_value(const char *tz)
19531968
{
19541969
size_t tz_len = strlen(tz);
1955-
if (tz_len > TZ_BUFFER_SIZE - 4) {
1956-
tz_len = TZ_BUFFER_SIZE - 4;
1970+
if (tz_len > TZ_MAX_VALUE_LEN) {
1971+
return false;
19571972
}
19581973
if (!tz_buffer_installed) {
1959-
memset(tz_buffer + 3, ' ', TZ_BUFFER_SIZE - 4);
1960-
tz_buffer[TZ_BUFFER_SIZE - 1] = '\0';
1961-
putenv(tz_buffer);
1974+
memset(tz_buffer + 3, 0, TZ_BUFFER_SIZE - 3);
1975+
if (putenv(tz_buffer) != 0) {
1976+
return false;
1977+
}
19621978
tz_buffer_installed = true;
19631979
tz_env_value = getenv("TZ");
19641980
if (tz_env_value == NULL) {
19651981
tz_env_value = tz_buffer + 3;
19661982
}
19671983
}
19681984
memcpy(tz_env_value, tz, tz_len);
1969-
memset(tz_env_value + tz_len, ' ', (TZ_BUFFER_SIZE - 4) - tz_len);
1970-
tz_env_value[TZ_BUFFER_SIZE - 4] = '\0';
1985+
memset(tz_env_value + tz_len, 0, TZ_MAX_VALUE_LEN - tz_len);
1986+
return true;
19711987
}
19721988

1989+
#endif // AVM_TZ_SETENV_LEAKS
1990+
19731991
term nif_erlang_localtime(Context *ctx, int argc, term argv[])
19741992
{
19751993
char *tz;
@@ -1997,15 +2015,38 @@ term nif_erlang_localtime(Context *ctx, int argc, term argv[])
19972015
char *oldtz_env = getenv("TZ");
19982016
if (oldtz_env) {
19992017
oldtz = strdup(oldtz_env);
2018+
if (UNLIKELY(oldtz == NULL)) {
2019+
#ifndef AVM_NO_SMP
2020+
smp_spinlock_unlock(&ctx->global->env_spinlock);
2021+
#endif
2022+
free(tz);
2023+
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
2024+
}
20002025
}
2026+
2027+
#if AVM_TZ_SETENV_LEAKS
20012028
set_tz_value(tz);
2029+
#else
2030+
setenv("TZ", tz, 1);
2031+
#endif
20022032
tzset();
20032033
localtime = localtime_r(&ts.tv_sec, &storage);
2034+
20042035
if (oldtz) {
2036+
#if AVM_TZ_SETENV_LEAKS
20052037
set_tz_value(oldtz);
2038+
#else
2039+
setenv("TZ", oldtz, 1);
2040+
#endif
20062041
free(oldtz);
20072042
} else {
2008-
set_tz_value("");
2043+
#if AVM_TZ_SETENV_LEAKS
2044+
// Cannot truly unset TZ with the static buffer approach.
2045+
// Setting to "UTC0" gives well-defined UTC behavior.
2046+
set_tz_value("UTC0");
2047+
#else
2048+
unsetenv("TZ");
2049+
#endif
20092050
}
20102051
tzset();
20112052
} else {
@@ -2017,6 +2058,11 @@ term nif_erlang_localtime(Context *ctx, int argc, term argv[])
20172058
#endif
20182059

20192060
free(tz);
2061+
2062+
if (UNLIKELY(localtime == NULL)) {
2063+
RAISE_ERROR(BADARG_ATOM);
2064+
}
2065+
20202066
return build_datetime_from_tm(ctx, localtime);
20212067
}
20222068

0 commit comments

Comments
 (0)