Skip to content

Commit d791eaf

Browse files
committed
Fix localtime/1 memory leak on newlib/picolibc platforms
Both newlib and picolibc leak the old "NAME=value" string when setenv overwrites an environment variable with a longer value. On ESP32 this causes unbounded heap growth when localtime/1 is called repeatedly with a timezone argument. Fix by replacing setenv/unsetenv with a static putenv buffer that is installed once and modified in place, avoiding all further allocations. The workaround is guarded by __NEWLIB__/__PICOLIBC__ so platforms with a non-leaking libc (glibc, musl) continue to use standard setenv/unsetenv. 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, giving well-defined UTC behavior on newlib/picolibc - Check strdup for OOM - Check putenv return value - Check localtime_r for NULL See: espressif/esp-idf#3046 See: espressif/esp-idf#9764 Signed-off-by: Peter M <petermm@gmail.com>
1 parent cef8c1a commit d791eaf

1 file changed

Lines changed: 84 additions & 2 deletions

File tree

src/libAtomVM/nifs.c

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,6 +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/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.
1945+
// 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
1954+
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.
1957+
#define TZ_BUFFER_SIZE 64
1958+
#define TZ_MAX_VALUE_LEN (TZ_BUFFER_SIZE - 4) // 3 for "TZ=" + 1 for '\0'
1959+
1960+
static char tz_buffer[TZ_BUFFER_SIZE] = "TZ=";
1961+
static bool tz_buffer_installed = false;
1962+
static char *tz_env_value = NULL;
1963+
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)
1968+
{
1969+
size_t tz_len = strlen(tz);
1970+
if (tz_len > TZ_MAX_VALUE_LEN) {
1971+
return false;
1972+
}
1973+
if (!tz_buffer_installed) {
1974+
memset(tz_buffer + 3, 0, TZ_BUFFER_SIZE - 3);
1975+
if (putenv(tz_buffer) != 0) {
1976+
return false;
1977+
}
1978+
tz_buffer_installed = true;
1979+
tz_env_value = getenv("TZ");
1980+
if (tz_env_value == NULL) {
1981+
tz_env_value = tz_buffer + 3;
1982+
}
1983+
}
1984+
memcpy(tz_env_value, tz, tz_len);
1985+
memset(tz_env_value + tz_len, 0, TZ_MAX_VALUE_LEN - tz_len);
1986+
return true;
1987+
}
1988+
1989+
#endif // AVM_TZ_SETENV_LEAKS
1990+
19421991
term nif_erlang_localtime(Context *ctx, int argc, term argv[])
19431992
{
19441993
char *tz;
@@ -1962,17 +2011,45 @@ term nif_erlang_localtime(Context *ctx, int argc, term argv[])
19622011
smp_spinlock_lock(&ctx->global->env_spinlock);
19632012
#endif
19642013
if (tz) {
1965-
char *oldtz = getenv("TZ");
2014+
char *oldtz = NULL;
2015+
char *oldtz_env = getenv("TZ");
2016+
if (oldtz_env) {
2017+
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+
}
2025+
}
2026+
2027+
#if AVM_TZ_SETENV_LEAKS
2028+
set_tz_value(tz);
2029+
#else
19662030
setenv("TZ", tz, 1);
2031+
#endif
19672032
tzset();
19682033
localtime = localtime_r(&ts.tv_sec, &storage);
2034+
19692035
if (oldtz) {
2036+
#if AVM_TZ_SETENV_LEAKS
2037+
set_tz_value(oldtz);
2038+
#else
19702039
setenv("TZ", oldtz, 1);
2040+
#endif
2041+
free(oldtz);
19712042
} else {
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
19722048
unsetenv("TZ");
2049+
#endif
19732050
}
2051+
tzset();
19742052
} else {
1975-
// Call tzset to handle DST changes
19762053
tzset();
19772054
localtime = localtime_r(&ts.tv_sec, &storage);
19782055
}
@@ -1981,6 +2058,11 @@ term nif_erlang_localtime(Context *ctx, int argc, term argv[])
19812058
#endif
19822059

19832060
free(tz);
2061+
2062+
if (UNLIKELY(localtime == NULL)) {
2063+
RAISE_ERROR(BADARG_ATOM);
2064+
}
2065+
19842066
return build_datetime_from_tm(ctx, localtime);
19852067
}
19862068

0 commit comments

Comments
 (0)