Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Stop using deprecated `term_from_int32` on RP2 platform
- Stop using deprecated `term_from_int32` on ESP32 platform
- Fixed improper cast of ESP32 `event_data` for `WIFI_EVENT_AP_STA(DIS)CONNECTED` events
- Fixed `erlang:localtime/1` memory leak, use-after-free, and TZ restore bugs on newlib/picolibc

## [0.7.0-alpha.1] - 2026-04-06

Expand Down
112 changes: 110 additions & 2 deletions src/libAtomVM/nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,64 @@ term nif_erlang_universaltime_0(Context *ctx, int argc, term argv[])
return build_datetime_from_tm(ctx, gmtime_r(&ts.tv_sec, &broken_down_time));
}

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

#if AVM_TZ_SETENV_LEAKS

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

static char tz_buffer[TZ_BUFFER_SIZE] = "TZ=";
static bool tz_buffer_installed = false;
// Cached pointer into the environment; assumes AtomVM is the sole user of TZ
// (no external threads reading or writing TZ or calling time functions
// that depend on it during the temporary override).
static char *tz_env_value = NULL;

// Write a TZ value into the static buffer. Returns false if the value is
// too long to fit (the buffer is left unchanged in that case).
// Caller must hold env_spinlock.
static bool set_static_tz_value(const char *tz)
{
size_t tz_len = strlen(tz);
if (tz_len > TZ_MAX_VALUE_LEN) {
return false;
}
if (!tz_buffer_installed) {
// Install a full-width placeholder first. Some libc implementations
// copy the environment string instead of keeping our static buffer, and
// installing just "TZ=" would leave too little storage for later
// in-place updates.
memset(tz_buffer + 3, ' ', TZ_MAX_VALUE_LEN);
tz_buffer[3 + TZ_MAX_VALUE_LEN] = '\0';
if (putenv(tz_buffer) != 0) {
return false;
}
tz_buffer_installed = true;
tz_env_value = getenv("TZ");
if (tz_env_value == NULL) {
tz_env_value = tz_buffer + 3;
}
}
memcpy(tz_env_value, tz, tz_len);
memset(tz_env_value + tz_len, 0, TZ_MAX_VALUE_LEN - tz_len);
tz_env_value[TZ_MAX_VALUE_LEN] = '\0';
return true;
}

#endif // AVM_TZ_SETENV_LEAKS

term nif_erlang_localtime(Context *ctx, int argc, term argv[])
{
char *tz;
Expand All @@ -1972,17 +2030,62 @@ term nif_erlang_localtime(Context *ctx, int argc, term argv[])
smp_spinlock_lock(&ctx->global->env_spinlock);
#endif
if (tz) {
char *oldtz = getenv("TZ");
char *oldtz = NULL;
char *oldtz_env = getenv("TZ");
if (oldtz_env) {
oldtz = strdup(oldtz_env);
if (UNLIKELY(oldtz == NULL)) {
#ifndef AVM_NO_SMP
smp_spinlock_unlock(&ctx->global->env_spinlock);
#endif
free(tz);
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
}

#if AVM_TZ_SETENV_LEAKS
if (!set_static_tz_value(tz)) {
#ifndef AVM_NO_SMP
smp_spinlock_unlock(&ctx->global->env_spinlock);
#endif
free(oldtz);
free(tz);
RAISE_ERROR(BADARG_ATOM);
}
#else
setenv("TZ", tz, 1);
#endif
tzset();
localtime = localtime_r(&ts.tv_sec, &storage);

if (oldtz) {
#if AVM_TZ_SETENV_LEAKS
if (!set_static_tz_value(oldtz)) {
setenv("TZ", oldtz, 1);
tz_env_value = getenv("TZ");
}
#else
setenv("TZ", oldtz, 1);
#endif
free(oldtz);
} else {
#if AVM_TZ_SETENV_LEAKS
// Cannot truly unset TZ with the static buffer approach;
// putenv does not support removal.
// NOTE: This is a pragmatic approximation, not a true restore.
// When TZ was originally unset, setting it to "UTC0" permanently
// changes observable process state (getenv("TZ") will return
// "UTC0" instead of NULL). This is acceptable on newlib/picolibc
// embedded targets where unset TZ already defaults to UTC.
if (!set_static_tz_value("UTC0")) {
unsetenv("TZ");
}
#else
unsetenv("TZ");
#endif
}
tzset();
} else {
// Call tzset to handle DST changes
tzset();
localtime = localtime_r(&ts.tv_sec, &storage);
}
Expand All @@ -1991,6 +2094,11 @@ term nif_erlang_localtime(Context *ctx, int argc, term argv[])
#endif

free(tz);

if (UNLIKELY(localtime == NULL)) {
RAISE_ERROR(BADARG_ATOM);
}

return build_datetime_from_tm(ctx, localtime);
}

Expand Down
Loading