[libc-commits] [libc] bef95e5 - [libc][stdlib] Add unsetenv (#202422)
via libc-commits
libc-commits at lists.llvm.org
Thu Jun 25 07:07:13 PDT 2026
Author: Jeff Bailey
Date: 2026-06-25T15:07:08+01:00
New Revision: bef95e54674592946530fdda73e4fea7809263f3
URL: https://github.com/llvm/llvm-project/commit/bef95e54674592946530fdda73e4fea7809263f3
DIFF: https://github.com/llvm/llvm-project/commit/bef95e54674592946530fdda73e4fea7809263f3.diff
LOG: [libc][stdlib] Add unsetenv (#202422)
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.
Added:
libc/src/stdlib/linux/unsetenv.cpp
libc/src/stdlib/unsetenv.h
libc/test/integration/src/stdlib/unsetenv_test.cpp
Modified:
libc/config/linux/aarch64/entrypoints.txt
libc/config/linux/riscv/entrypoints.txt
libc/config/linux/x86_64/entrypoints.txt
libc/include/stdlib.yaml
libc/src/CMakeLists.txt
libc/src/stdlib/CMakeLists.txt
libc/src/stdlib/environ_internal.cpp
libc/src/stdlib/environ_internal.h
libc/src/stdlib/linux/CMakeLists.txt
libc/test/integration/src/stdlib/CMakeLists.txt
Removed:
################################################################################
diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index 599ae32a35073..746cd98a5a55c 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -1177,6 +1177,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 ef2d13e6d6f9e..9133d8fcb0b82 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1306,6 +1306,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 8046679785e68..278c6be1d8d84 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1372,6 +1372,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 bf77d5d850ea0..93f61d541b073 100644
--- a/libc/include/stdlib.yaml
+++ b/libc/include/stdlib.yaml
@@ -376,6 +376,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 56085c9632f59..62aa164b35ee7 100644
--- a/libc/src/CMakeLists.txt
+++ b/libc/src/CMakeLists.txt
@@ -14,11 +14,13 @@ add_subdirectory(netinet)
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)
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 4593288881c7d..1e26588ad7305 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("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_COPT_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(
@@ -677,6 +688,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..d7ef081e7e2b1 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_COPT_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_COPT_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_COPT_SUPPORT_ENVIRON
+ environ = storage;
+#endif
return true;
}
@@ -221,5 +230,40 @@ 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();
+
+ // 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);
+ }
+
+ 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..789f9aded9570 100644
--- a/libc/test/integration/src/stdlib/CMakeLists.txt
+++ b/libc/test/integration/src/stdlib/CMakeLists.txt
@@ -30,6 +30,22 @@ 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.__support.alloc_checker
+ libc.src.stdlib.getenv
+ libc.src.stdlib.setenv
+ libc.src.stdlib.unsetenv
+ libc.src.string.memory_utils.inline_memcpy
+ 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..53341940702ed
--- /dev/null
+++ b/libc/test/integration/src/stdlib/unsetenv_test.cpp
@@ -0,0 +1,176 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/__support/alloc-checker.h"
+#include "src/stdlib/environ_internal.h"
+#include "src/stdlib/getenv.h"
+#include "src/stdlib/setenv.h"
+#include "src/stdlib/unsetenv.h"
+#include "src/string/memory_utils/inline_memcpy.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 sets errno to EINVAL
+ {
+ errno = 0;
+ ASSERT_EQ(unsetenv(""), -1);
+ ASSERT_ERRNO_EQ(EINVAL);
+ }
+
+ // Test: Name with '=' sets errno to 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);
+ }
+
+ // 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);
+
+ AllocChecker ac;
+ char *d1 = new (ac) char[14];
+ ASSERT_TRUE(ac);
+ const char *s1 = "DUP_VAR=first";
+ inline_memcpy(d1, s1, 14);
+
+ char *d2 = new (ac) char[15];
+ ASSERT_TRUE(ac);
+ const char *s2 = "DUP_VAR=second";
+ inline_memcpy(d2, s2, 15);
+
+ 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;
+}
+
+} // namespace LIBC_NAMESPACE
More information about the libc-commits
mailing list