[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.
This commit is contained in:
Jeff Bailey authored and GitHub committed 2026-06-25 15:07:08 +01:00
1 parent ec574cf044
commit bef95e5467
13 files changed
+363 -10

No files matched your search

@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+6
View File
@@ -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
+3 -1
View File
@@ -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)
+27 -9
View File
@@ -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
+44
View File
@@ -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
+5
View File
@@ -129,6 +129,11 @@ public:
// 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
+15
View File
@@ -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
)
+43
View File
@@ -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
+25
View File
@@ -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
@@ -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
@@ -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