[libc-commits] [libc] [libc][stdlib] Add unsetenv (PR #202422)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Tue Jun 23 07:52:16 PDT 2026


https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/202422

>From f5cb195612f9deecd8c03f6f5cddaadaa9adf071 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Mon, 8 Jun 2026 20:37:56 +0100
Subject: [PATCH 1/3] [libc][stdlib] Add unsetenv

Added the POSIX unsetenv() function and its internal support.

Implemented EnvironmentManager::unset() to remove a variable by name,
free the string if allocated, and compact the array.

Updated EnvironmentManager to synchronize the public global environ
pointer when transitioning to managed storage.

Registered for x86_64, aarch64, and riscv. Integration tests cover
basic operations and edge cases.

Assisted-by: Automated tooling, human reviewed.
---
 libc/config/linux/aarch64/entrypoints.txt     |   1 +
 libc/config/linux/riscv/entrypoints.txt       |   1 +
 libc/config/linux/x86_64/entrypoints.txt      |   1 +
 libc/include/stdlib.yaml                      |   6 +
 libc/src/CMakeLists.txt                       |   2 +-
 libc/src/stdlib/CMakeLists.txt                |  36 +++--
 libc/src/stdlib/environ_internal.cpp          |  36 +++++
 libc/src/stdlib/environ_internal.h            |   5 +
 libc/src/stdlib/linux/CMakeLists.txt          |  15 ++
 libc/src/stdlib/linux/unsetenv.cpp            |  43 ++++++
 libc/src/stdlib/unsetenv.h                    |  25 ++++
 .../integration/src/stdlib/CMakeLists.txt     |  14 ++
 .../integration/src/stdlib/unsetenv_test.cpp  | 128 ++++++++++++++++++
 13 files changed, 303 insertions(+), 10 deletions(-)
 create mode 100644 libc/src/stdlib/linux/unsetenv.cpp
 create mode 100644 libc/src/stdlib/unsetenv.h
 create mode 100644 libc/test/integration/src/stdlib/unsetenv_test.cpp

diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index bc2055db769ab..ae9458a504a1a 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -1160,6 +1160,7 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdlib.exit
     libc.src.stdlib.getenv
     libc.src.stdlib.setenv
+    libc.src.stdlib.unsetenv
     libc.src.stdlib.quick_exit
 
     # signal.h entrypoints
diff --git a/libc/config/linux/riscv/entrypoints.txt b/libc/config/linux/riscv/entrypoints.txt
index 641125a61e22e..c8d64a7520ef9 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1289,6 +1289,7 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdlib.exit
     libc.src.stdlib.getenv
     libc.src.stdlib.setenv
+    libc.src.stdlib.unsetenv
     libc.src.stdlib.quick_exit
 
     # signal.h entrypoints
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index b6c79cd2b6c8b..062680a2dc296 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1355,6 +1355,7 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdlib.exit
     libc.src.stdlib.getenv
     libc.src.stdlib.setenv
+    libc.src.stdlib.unsetenv
     libc.src.stdlib.mbstowcs
     libc.src.stdlib.mbtowc
     libc.src.stdlib.mblen
diff --git a/libc/include/stdlib.yaml b/libc/include/stdlib.yaml
index 0ac0623237e5b..a02050edf1a31 100644
--- a/libc/include/stdlib.yaml
+++ b/libc/include/stdlib.yaml
@@ -370,6 +370,12 @@ functions:
     return_type: int
     arguments:
       - type: const char *
+  - name: unsetenv
+    standards:
+      - posix
+    return_type: int
+    arguments:
+      - type: const char *
   - name: wctomb
     standards:
       - stdc
diff --git a/libc/src/CMakeLists.txt b/libc/src/CMakeLists.txt
index 56fb121ecda75..93ed89e2fb267 100644
--- a/libc/src/CMakeLists.txt
+++ b/libc/src/CMakeLists.txt
@@ -12,11 +12,11 @@ add_subdirectory(math)
 add_subdirectory(stdbit)
 add_subdirectory(stdfix)
 add_subdirectory(stdio)
+add_subdirectory(unistd)
 add_subdirectory(stdlib)
 add_subdirectory(string)
 add_subdirectory(strings)
 add_subdirectory(time)
-add_subdirectory(unistd)
 add_subdirectory(wchar)
 add_subdirectory(wctype)
 
diff --git a/libc/src/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 7e542e56a983d..1303fb3e46228 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -77,6 +77,24 @@ if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") AND
       "-fdisable-tree-waccess3")
 endif()
 
+set(environ_internal_deps
+  libc.config.app_h
+  libc.hdr.types.size_t
+  libc.src.__support.alloc_checker
+  libc.src.__support.CPP.new
+  libc.src.__support.CPP.optional
+  libc.src.__support.CPP.string_view
+  libc.src.__support.macros.attributes
+  libc.src.__support.macros.config
+  libc.src.string.memory_utils.inline_memcpy
+)
+set(environ_internal_compile_options ${environ_internal_gcc12_workaround})
+
+if("environ" IN_LIST TARGET_ENTRYPOINT_NAME_LIST)
+  list(APPEND environ_internal_deps libc.src.unistd.environ)
+  list(APPEND environ_internal_compile_options "-DLIBC_SUPPORT_ENVIRON")
+endif()
+
 add_object_library(
   environ_internal
   SRCS
@@ -84,16 +102,9 @@ add_object_library(
   HDRS
     environ_internal.h
   COMPILE_OPTIONS
-    ${environ_internal_gcc12_workaround}
+    ${environ_internal_compile_options}
   DEPENDS
-    libc.config.app_h
-    libc.hdr.types.size_t
-    libc.src.__support.CPP.new
-    libc.src.__support.CPP.optional
-    libc.src.__support.CPP.string_view
-    libc.src.__support.macros.attributes
-    libc.src.__support.macros.config
-    libc.src.string.memory_utils.inline_memcpy
+    ${environ_internal_deps}
 )
 
 add_entrypoint_object(
@@ -660,6 +671,13 @@ add_entrypoint_object(
     .${LIBC_TARGET_OS}.setenv
 )
 
+add_entrypoint_object(
+  unsetenv
+  ALIAS
+  DEPENDS
+    .${LIBC_TARGET_OS}.unsetenv
+)
+
 if(LIBC_TARGET_OS_IS_BAREMETAL OR LIBC_TARGET_OS_IS_GPU)
   add_entrypoint_object(
     malloc
diff --git a/libc/src/stdlib/environ_internal.cpp b/libc/src/stdlib/environ_internal.cpp
index e9da17d028575..3c780f0729d0a 100644
--- a/libc/src/stdlib/environ_internal.cpp
+++ b/libc/src/stdlib/environ_internal.cpp
@@ -18,6 +18,9 @@
 #include "src/__support/alloc-checker.h"
 #include "src/__support/macros/config.h"
 #include "src/string/memory_utils/inline_memcpy.h"
+#ifdef LIBC_SUPPORT_ENVIRON
+#include "src/unistd/environ.h"
+#endif
 
 namespace LIBC_NAMESPACE_DECL {
 namespace internal {
@@ -142,6 +145,9 @@ bool EnvironmentManager::ensure_capacity(size_t needed) {
 
     // Update the global environ pointer.
     app.env_ptr = reinterpret_cast<uintptr_t *>(storage);
+#ifdef LIBC_SUPPORT_ENVIRON
+    environ = storage;
+#endif
 
     return true;
   }
@@ -168,6 +174,9 @@ bool EnvironmentManager::ensure_capacity(size_t needed) {
 
   // Update the global environ pointer.
   app.env_ptr = reinterpret_cast<uintptr_t *>(storage);
+#ifdef LIBC_SUPPORT_ENVIRON
+  environ = storage;
+#endif
 
   return true;
 }
@@ -221,5 +230,32 @@ int EnvironmentManager::set(cpp::string_view name, cpp::string_view value,
   return 0;
 }
 
+int EnvironmentManager::unset(cpp::string_view name) {
+  cpp::optional<size_t> idx = find_var(name);
+  if (!idx)
+    return 0; // Variable not found; POSIX defines this as success.
+
+  // Transition to managed storage so we can modify the array and track
+  // ownership correctly.
+  if (!ensure_capacity(count))
+    return -1;
+
+  char **env_array = get_array();
+
+  // Free the string if we allocated it.
+  if (ownership[*idx].can_free())
+    delete[] env_array[*idx];
+
+  // Compact: shift remaining entries left to fill the gap.
+  for (size_t i = *idx; i < count - 1; i++) {
+    env_array[i] = env_array[i + 1];
+    ownership[i] = ownership[i + 1];
+  }
+  count--;
+  env_array[count] = nullptr;
+
+  return 0;
+}
+
 } // namespace internal
 } // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdlib/environ_internal.h b/libc/src/stdlib/environ_internal.h
index d902354f421cf..f16d841779734 100644
--- a/libc/src/stdlib/environ_internal.h
+++ b/libc/src/stdlib/environ_internal.h
@@ -129,6 +129,11 @@ class EnvironmentManager {
   // Returns 0 on success, -1 on allocation failure (caller should set
   // errno to ENOMEM).
   int set(cpp::string_view name, cpp::string_view value, bool overwrite);
+
+  // Remove a variable by name. Frees the string if we own it, then
+  // compacts the array. Returns 0 on success (including if the variable
+  // was not found), -1 on allocation failure during array transition.
+  int unset(cpp::string_view name);
 };
 
 } // namespace internal
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index aedcd3f11bf38..06af6f046afa1 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -23,3 +23,18 @@ add_entrypoint_object(
     libc.src.__support.macros.null_check
     libc.src.stdlib.environ_internal
 )
+
+add_entrypoint_object(
+  unsetenv
+  SRCS
+    unsetenv.cpp
+  HDRS
+    ../unsetenv.h
+  DEPENDS
+    libc.src.__support.CPP.string_view
+    libc.src.__support.common
+    libc.src.__support.libc_errno
+    libc.src.__support.macros.config
+    libc.src.__support.macros.null_check
+    libc.src.stdlib.environ_internal
+)
diff --git a/libc/src/stdlib/linux/unsetenv.cpp b/libc/src/stdlib/linux/unsetenv.cpp
new file mode 100644
index 0000000000000..538242ed29fca
--- /dev/null
+++ b/libc/src/stdlib/linux/unsetenv.cpp
@@ -0,0 +1,43 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Implementation of the POSIX unsetenv function.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/unsetenv.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/common.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/macros/null_check.h"
+#include "src/stdlib/environ_internal.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(int, unsetenv, (const char *name)) {
+  LIBC_CRASH_ON_NULLPTR(name);
+
+  cpp::string_view name_view(name);
+
+  // POSIX: name must not be empty or contain '='.
+  if (name_view.empty() ||
+      name_view.find_first_of('=') != cpp::string_view::npos) {
+    libc_errno = EINVAL;
+    return -1;
+  }
+
+  int result = internal::EnvironmentManager::get_instance().unset(name_view);
+  if (result != 0)
+    libc_errno = ENOMEM;
+
+  return result;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdlib/unsetenv.h b/libc/src/stdlib/unsetenv.h
new file mode 100644
index 0000000000000..b705d1b481aef
--- /dev/null
+++ b/libc/src/stdlib/unsetenv.h
@@ -0,0 +1,25 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Declaration of the POSIX unsetenv function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDLIB_UNSETENV_H
+#define LLVM_LIBC_SRC_STDLIB_UNSETENV_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int unsetenv(const char *name);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDLIB_UNSETENV_H
diff --git a/libc/test/integration/src/stdlib/CMakeLists.txt b/libc/test/integration/src/stdlib/CMakeLists.txt
index 2e0389f9a60d3..e7923931a03dd 100644
--- a/libc/test/integration/src/stdlib/CMakeLists.txt
+++ b/libc/test/integration/src/stdlib/CMakeLists.txt
@@ -30,6 +30,20 @@ if(${LIBC_TARGET_OS} STREQUAL "linux")
       libc.src.string.strcmp
   )
 
+  add_integration_test(
+    unsetenv_test
+    SUITE
+      stdlib-integration-tests
+    SRCS
+      unsetenv_test.cpp
+    DEPENDS
+      libc.src.stdlib.getenv
+      libc.src.stdlib.setenv
+      libc.src.stdlib.unsetenv
+      libc.src.string.strcmp
+      libc.src.unistd.environ
+  )
+
   add_integration_test(
     abort_test
     SUITE
diff --git a/libc/test/integration/src/stdlib/unsetenv_test.cpp b/libc/test/integration/src/stdlib/unsetenv_test.cpp
new file mode 100644
index 0000000000000..979ed16bfe4a5
--- /dev/null
+++ b/libc/test/integration/src/stdlib/unsetenv_test.cpp
@@ -0,0 +1,128 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+/// \file
+/// Integration tests for unsetenv.
+
+#include "src/stdlib/getenv.h"
+#include "src/stdlib/setenv.h"
+#include "src/stdlib/unsetenv.h"
+#include "src/string/strcmp.h"
+#include "src/unistd/environ.h"
+
+#include "test/IntegrationTest/test.h"
+
+#include <errno.h>
+
+namespace LIBC_NAMESPACE {
+
+TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
+          [[maybe_unused]] char **envp) {
+  // Test: Remove a variable set by setenv
+  {
+    ASSERT_EQ(setenv("UNSET_VAR", "value", 1), 0);
+    ASSERT_TRUE(getenv("UNSET_VAR") != nullptr);
+
+    ASSERT_EQ(unsetenv("UNSET_VAR"), 0);
+    ASSERT_TRUE(getenv("UNSET_VAR") == nullptr);
+  }
+
+  // Test: Unset non-existent variable succeeds
+  {
+    ASSERT_EQ(unsetenv("DOES_NOT_EXIST"), 0);
+  }
+
+  // Test: Empty name returns EINVAL
+  {
+    errno = 0;
+    ASSERT_EQ(unsetenv(""), -1);
+    ASSERT_ERRNO_EQ(EINVAL);
+  }
+
+  // Test: Name with '=' returns EINVAL
+  {
+    errno = 0;
+    ASSERT_EQ(unsetenv("BAD=NAME"), -1);
+    ASSERT_ERRNO_EQ(EINVAL);
+  }
+
+  // Test: Unset then re-set
+  {
+    ASSERT_EQ(setenv("REUSE_VAR", "first", 1), 0);
+    ASSERT_EQ(strcmp(getenv("REUSE_VAR"), "first"), 0);
+
+    ASSERT_EQ(unsetenv("REUSE_VAR"), 0);
+    ASSERT_TRUE(getenv("REUSE_VAR") == nullptr);
+
+    ASSERT_EQ(setenv("REUSE_VAR", "second", 1), 0);
+    ASSERT_EQ(strcmp(getenv("REUSE_VAR"), "second"), 0);
+  }
+
+  // Test: Unset multiple variables
+  {
+    ASSERT_EQ(setenv("MULTI_A", "a", 1), 0);
+    ASSERT_EQ(setenv("MULTI_B", "b", 1), 0);
+    ASSERT_EQ(setenv("MULTI_C", "c", 1), 0);
+
+    ASSERT_EQ(unsetenv("MULTI_B"), 0);
+
+    ASSERT_TRUE(getenv("MULTI_A") != nullptr);
+    ASSERT_TRUE(getenv("MULTI_B") == nullptr);
+    ASSERT_TRUE(getenv("MULTI_C") != nullptr);
+
+    ASSERT_EQ(strcmp(getenv("MULTI_A"), "a"), 0);
+    ASSERT_EQ(strcmp(getenv("MULTI_C"), "c"), 0);
+  }
+
+  // Test: Unset same variable twice is harmless
+  {
+    ASSERT_EQ(setenv("DOUBLE_UNSET", "val", 1), 0);
+    ASSERT_EQ(unsetenv("DOUBLE_UNSET"), 0);
+    ASSERT_EQ(unsetenv("DOUBLE_UNSET"), 0);
+    ASSERT_TRUE(getenv("DOUBLE_UNSET") == nullptr);
+  }
+
+  // Test: environ is updated and does NOT contain the variable
+  {
+    ASSERT_EQ(setenv("ENV_CHECK", "val", 1), 0);
+    // Verify it is in environ
+    bool found = false;
+    for (char **env = environ; *env != nullptr; ++env) {
+      if (strcmp(*env, "ENV_CHECK=val") == 0) {
+        found = true;
+        break;
+      }
+    }
+    ASSERT_TRUE(found);
+
+    // Now unset it
+    ASSERT_EQ(unsetenv("ENV_CHECK"), 0);
+
+    // Verify it is NOT in environ
+    found = false;
+    for (char **env = environ; *env != nullptr; ++env) {
+      bool match = true;
+      const char *prefix = "ENV_CHECK=";
+      for (size_t i = 0; i < 10; ++i) {
+        if ((*env)[i] == '\0' || (*env)[i] != prefix[i]) {
+          match = false;
+          break;
+        }
+      }
+      if (match) {
+        found = true;
+        break;
+      }
+    }
+    ASSERT_FALSE(found);
+  }
+
+  return 0;
+}
+
+} // namespace LIBC_NAMESPACE

>From 653829ac25c207cb167acbfe134c08274728be53 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Tue, 23 Jun 2026 15:48:01 +0100
Subject: [PATCH 2/3] [libc] Address review feedback for unsetenv (#202422)

Address feedback from PR 202422:

* Update CMake configuration to use the full target name
  libc.src.unistd.environ in TARGET_LLVMLIBC_ENTRYPOINTS check.
* Rename compile option macro from LIBC_SUPPORT_ENVIRON to
  LIBC_COPT_SUPPORT_ENVIRON to follow project conventions.
* Add comment in libc/src/CMakeLists.txt explaining unistd/stdlib
  directory ordering.
* Add comment in EnvironmentManager::unset explaining compacting by
  shifting elements to preserve environment order.
* Correct file header format in unsetenv_test.cpp and fix comment nits.
* Update EnvironmentManager::unset to remove all instances of a
  variable to handle duplicates, and add an integration test case.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/CMakeLists.txt                       |  2 +
 libc/src/stdlib/CMakeLists.txt                |  4 +-
 libc/src/stdlib/environ_internal.cpp          | 34 +++++++-----
 .../integration/src/stdlib/unsetenv_test.cpp  | 53 +++++++++++++++++--
 4 files changed, 74 insertions(+), 19 deletions(-)

diff --git a/libc/src/CMakeLists.txt b/libc/src/CMakeLists.txt
index 93ed89e2fb267..b8524693de3ad 100644
--- a/libc/src/CMakeLists.txt
+++ b/libc/src/CMakeLists.txt
@@ -12,6 +12,8 @@ add_subdirectory(math)
 add_subdirectory(stdbit)
 add_subdirectory(stdfix)
 add_subdirectory(stdio)
+# unistd must be added before stdlib because stdlib's environ_internal
+# target conditionally depends on unistd's environ target.
 add_subdirectory(unistd)
 add_subdirectory(stdlib)
 add_subdirectory(string)
diff --git a/libc/src/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 1303fb3e46228..348c64aa5e995 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -90,9 +90,9 @@ set(environ_internal_deps
 )
 set(environ_internal_compile_options ${environ_internal_gcc12_workaround})
 
-if("environ" IN_LIST TARGET_ENTRYPOINT_NAME_LIST)
+if("libc.src.unistd.environ" IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS)
   list(APPEND environ_internal_deps libc.src.unistd.environ)
-  list(APPEND environ_internal_compile_options "-DLIBC_SUPPORT_ENVIRON")
+  list(APPEND environ_internal_compile_options "-DLIBC_COPT_SUPPORT_ENVIRON")
 endif()
 
 add_object_library(
diff --git a/libc/src/stdlib/environ_internal.cpp b/libc/src/stdlib/environ_internal.cpp
index 3c780f0729d0a..d7ef081e7e2b1 100644
--- a/libc/src/stdlib/environ_internal.cpp
+++ b/libc/src/stdlib/environ_internal.cpp
@@ -18,7 +18,7 @@
 #include "src/__support/alloc-checker.h"
 #include "src/__support/macros/config.h"
 #include "src/string/memory_utils/inline_memcpy.h"
-#ifdef LIBC_SUPPORT_ENVIRON
+#ifdef LIBC_COPT_SUPPORT_ENVIRON
 #include "src/unistd/environ.h"
 #endif
 
@@ -145,7 +145,7 @@ bool EnvironmentManager::ensure_capacity(size_t needed) {
 
     // Update the global environ pointer.
     app.env_ptr = reinterpret_cast<uintptr_t *>(storage);
-#ifdef LIBC_SUPPORT_ENVIRON
+#ifdef LIBC_COPT_SUPPORT_ENVIRON
     environ = storage;
 #endif
 
@@ -174,7 +174,7 @@ bool EnvironmentManager::ensure_capacity(size_t needed) {
 
   // Update the global environ pointer.
   app.env_ptr = reinterpret_cast<uintptr_t *>(storage);
-#ifdef LIBC_SUPPORT_ENVIRON
+#ifdef LIBC_COPT_SUPPORT_ENVIRON
   environ = storage;
 #endif
 
@@ -242,17 +242,25 @@ int EnvironmentManager::unset(cpp::string_view name) {
 
   char **env_array = get_array();
 
-  // Free the string if we allocated it.
-  if (ownership[*idx].can_free())
-    delete[] env_array[*idx];
-
-  // Compact: shift remaining entries left to fill the gap.
-  for (size_t i = *idx; i < count - 1; i++) {
-    env_array[i] = env_array[i + 1];
-    ownership[i] = ownership[i + 1];
+  // Loop to remove all instances of the variable (e.g. duplicates from execve).
+  while (idx) {
+    size_t i = *idx;
+    if (ownership[i].can_free())
+      delete[] env_array[i];
+
+    // Compact: shift remaining entries left to fill the gap.
+    // Shifting elements preserves the order of environment variables, which is
+    // desirable to maintain consistency with getenv resolution order and
+    // typical environ iteration behavior.
+    for (size_t j = i; j < count - 1; j++) {
+      env_array[j] = env_array[j + 1];
+      ownership[j] = ownership[j + 1];
+    }
+    count--;
+    env_array[count] = nullptr;
+
+    idx = find_var(name);
   }
-  count--;
-  env_array[count] = nullptr;
 
   return 0;
 }
diff --git a/libc/test/integration/src/stdlib/unsetenv_test.cpp b/libc/test/integration/src/stdlib/unsetenv_test.cpp
index 979ed16bfe4a5..5b2193ba34236 100644
--- a/libc/test/integration/src/stdlib/unsetenv_test.cpp
+++ b/libc/test/integration/src/stdlib/unsetenv_test.cpp
@@ -1,17 +1,20 @@
-//===----------------------------------------------------------------------===//
+//===-- Integration tests for unsetenv -----------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
-
+///
 /// \file
 /// Integration tests for unsetenv.
+///
+//===----------------------------------------------------------------------===//
 
 #include "src/stdlib/getenv.h"
 #include "src/stdlib/setenv.h"
 #include "src/stdlib/unsetenv.h"
+#include "src/stdlib/environ_internal.h"
 #include "src/string/strcmp.h"
 #include "src/unistd/environ.h"
 
@@ -37,14 +40,14 @@ TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
     ASSERT_EQ(unsetenv("DOES_NOT_EXIST"), 0);
   }
 
-  // Test: Empty name returns EINVAL
+  // Test: Empty name sets errno to EINVAL
   {
     errno = 0;
     ASSERT_EQ(unsetenv(""), -1);
     ASSERT_ERRNO_EQ(EINVAL);
   }
 
-  // Test: Name with '=' returns EINVAL
+  // Test: Name with '=' sets errno to EINVAL
   {
     errno = 0;
     ASSERT_EQ(unsetenv("BAD=NAME"), -1);
@@ -122,6 +125,48 @@ TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
     ASSERT_FALSE(found);
   }
 
+  // Test: Unset removes all duplicate instances of a variable
+  {
+    ASSERT_EQ(setenv("DUP_VAL1", "1", 1), 0);
+    ASSERT_EQ(setenv("DUP_VAL2", "2", 1), 0);
+
+    char *d1 = new char[14];
+    const char *s1 = "DUP_VAR=first";
+    for (size_t i = 0; i < 14; ++i)
+      d1[i] = s1[i];
+
+    char *d2 = new char[15];
+    const char *s2 = "DUP_VAR=second";
+    for (size_t i = 0; i < 15; ++i)
+      d2[i] = s2[i];
+
+    internal::EnvironmentManager &mgr =
+        internal::EnvironmentManager::get_instance();
+    char **env_array = mgr.begin();
+    size_t count = mgr.size();
+
+    size_t idx1 = count;
+    size_t idx2 = count;
+    for (size_t i = 0; i < count; ++i) {
+      cpp::string_view curr(env_array[i]);
+      if (curr.starts_with("DUP_VAL1="))
+        idx1 = i;
+      if (curr.starts_with("DUP_VAL2="))
+        idx2 = i;
+    }
+    ASSERT_TRUE(idx1 < count);
+    ASSERT_TRUE(idx2 < count);
+
+    delete[] env_array[idx1];
+    delete[] env_array[idx2];
+
+    env_array[idx1] = d1;
+    env_array[idx2] = d2;
+
+    ASSERT_EQ(unsetenv("DUP_VAR"), 0);
+    ASSERT_TRUE(getenv("DUP_VAR") == nullptr);
+  }
+
   return 0;
 }
 

>From e8b7f2b4e2810db04efe74b34a0bf8eeb49821a6 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Tue, 23 Jun 2026 15:52:00 +0100
Subject: [PATCH 3/3] [libc] Fix unsetenv_test compilation by using
 AllocChecker

Fix linker errors in unsetenv_test by using AllocChecker for memory
allocation in the duplicate test case, avoiding standard C++ operator
new/delete dependencies.

Assisted-by: Automated tooling, human reviewed.
---
 libc/test/integration/src/stdlib/CMakeLists.txt    | 1 +
 libc/test/integration/src/stdlib/unsetenv_test.cpp | 8 ++++++--
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/libc/test/integration/src/stdlib/CMakeLists.txt b/libc/test/integration/src/stdlib/CMakeLists.txt
index e7923931a03dd..e0805f184b292 100644
--- a/libc/test/integration/src/stdlib/CMakeLists.txt
+++ b/libc/test/integration/src/stdlib/CMakeLists.txt
@@ -42,6 +42,7 @@ if(${LIBC_TARGET_OS} STREQUAL "linux")
       libc.src.stdlib.unsetenv
       libc.src.string.strcmp
       libc.src.unistd.environ
+      libc.src.__support.alloc_checker
   )
 
   add_integration_test(
diff --git a/libc/test/integration/src/stdlib/unsetenv_test.cpp b/libc/test/integration/src/stdlib/unsetenv_test.cpp
index 5b2193ba34236..f79a46cdf2151 100644
--- a/libc/test/integration/src/stdlib/unsetenv_test.cpp
+++ b/libc/test/integration/src/stdlib/unsetenv_test.cpp
@@ -14,6 +14,7 @@
 #include "src/stdlib/getenv.h"
 #include "src/stdlib/setenv.h"
 #include "src/stdlib/unsetenv.h"
+#include "src/__support/alloc-checker.h"
 #include "src/stdlib/environ_internal.h"
 #include "src/string/strcmp.h"
 #include "src/unistd/environ.h"
@@ -130,12 +131,15 @@ TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
     ASSERT_EQ(setenv("DUP_VAL1", "1", 1), 0);
     ASSERT_EQ(setenv("DUP_VAL2", "2", 1), 0);
 
-    char *d1 = new char[14];
+    AllocChecker ac;
+    char *d1 = new (ac) char[14];
+    ASSERT_TRUE(ac);
     const char *s1 = "DUP_VAR=first";
     for (size_t i = 0; i < 14; ++i)
       d1[i] = s1[i];
 
-    char *d2 = new char[15];
+    char *d2 = new (ac) char[15];
+    ASSERT_TRUE(ac);
     const char *s2 = "DUP_VAR=second";
     for (size_t i = 0; i < 15; ++i)
       d2[i] = s2[i];



More information about the libc-commits mailing list