[libc-commits] [libc] e362f3c - [libc][stdlib] Add putenv (#208339)
via libc-commits
libc-commits at lists.llvm.org
Tue Jul 28 07:35:09 PDT 2026
Author: Jeff Bailey
Date: 2026-07-28T15:35:04+01:00
New Revision: e362f3c8466dfb4021d409f50eae179b9b0b1957
URL: https://github.com/llvm/llvm-project/commit/e362f3c8466dfb4021d409f50eae179b9b0b1957
DIFF: https://github.com/llvm/llvm-project/commit/e362f3c8466dfb4021d409f50eae179b9b0b1957.diff
LOG: [libc][stdlib] Add putenv (#208339)
Added the POSIX putenv() function and its internal support.
Implemented EnvironmentManager::put() to insert caller-provided
"name=value" strings directly into the environment array, managing
ownership correctly (caller retains ownership).
Registered for x86_64, aarch64, and riscv. Integration tests cover basic
operations, ownership semantics, validation, and edge cases.
Assisted-by: Automated tooling, human reviewed.
Added:
libc/src/stdlib/linux/putenv.cpp
libc/src/stdlib/putenv.h
libc/test/integration/src/stdlib/putenv_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/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 046fd301ea01b..2cfbc1e2b69fc 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -1207,6 +1207,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.stdlib.atexit
libc.src.stdlib.exit
libc.src.stdlib.getenv
+ libc.src.stdlib.putenv
libc.src.stdlib.setenv
libc.src.stdlib.unsetenv
libc.src.stdlib.quick_exit
diff --git a/libc/config/linux/riscv/entrypoints.txt b/libc/config/linux/riscv/entrypoints.txt
index c6a4b431f33de..256e3a7dc3a87 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1400,6 +1400,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.stdlib.atexit
libc.src.stdlib.exit
libc.src.stdlib.getenv
+ libc.src.stdlib.putenv
libc.src.stdlib.setenv
libc.src.stdlib.unsetenv
libc.src.stdlib.mbstowcs
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index a86875fa4b9e8..619fc9b85ae48 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1409,6 +1409,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.stdlib.atexit
libc.src.stdlib.exit
libc.src.stdlib.getenv
+ libc.src.stdlib.putenv
libc.src.stdlib.setenv
libc.src.stdlib.unsetenv
libc.src.stdlib.mbstowcs
diff --git a/libc/include/stdlib.yaml b/libc/include/stdlib.yaml
index 0533fe04aecbb..efbd47029199e 100644
--- a/libc/include/stdlib.yaml
+++ b/libc/include/stdlib.yaml
@@ -185,6 +185,12 @@ functions:
- type: void **
- type: size_t
- type: size_t
+ - name: putenv
+ standards:
+ - posix
+ return_type: int
+ arguments:
+ - type: char *
- name: qsort
standards:
- stdc
diff --git a/libc/src/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 7df0a2b0c4656..64743cd5295aa 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -695,6 +695,13 @@ add_entrypoint_object(
.${LIBC_TARGET_OS}.setenv
)
+add_entrypoint_object(
+ putenv
+ ALIAS
+ DEPENDS
+ .${LIBC_TARGET_OS}.putenv
+)
+
add_entrypoint_object(
unsetenv
ALIAS
diff --git a/libc/src/stdlib/environ_internal.cpp b/libc/src/stdlib/environ_internal.cpp
index d7ef081e7e2b1..da2e5c8185ca0 100644
--- a/libc/src/stdlib/environ_internal.cpp
+++ b/libc/src/stdlib/environ_internal.cpp
@@ -265,5 +265,41 @@ int EnvironmentManager::unset(cpp::string_view name) {
return 0;
}
+int EnvironmentManager::put(char *string) {
+ cpp::string_view sv(string);
+ size_t eq_pos = sv.find_first_of('=');
+
+ // No '=' found: treat as unset (POSIX behavior).
+ if (eq_pos == cpp::string_view::npos)
+ return unset(sv);
+
+ cpp::string_view name = sv.substr(0, eq_pos);
+
+ cpp::optional<size_t> idx = find_var(name);
+
+ size_t needed = idx ? count : count + 1;
+ if (!ensure_capacity(needed))
+ return -1;
+
+ char **env_array = get_array();
+
+ if (idx) {
+ // Replace existing variable. Free old string if we own it.
+ if (ownership[*idx].can_free())
+ delete[] env_array[*idx];
+
+ env_array[*idx] = string;
+ ownership[*idx].allocated_by_us = false; // Caller owns this string.
+ } else {
+ // Add new variable at the end.
+ env_array[count] = string;
+ ownership[count].allocated_by_us = false;
+ 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 f16d841779734..aa111b8496aec 100644
--- a/libc/src/stdlib/environ_internal.h
+++ b/libc/src/stdlib/environ_internal.h
@@ -130,6 +130,12 @@ class EnvironmentManager {
// errno to ENOMEM).
int set(cpp::string_view name, cpp::string_view value, bool overwrite);
+ // Insert a caller-provided "name=value" string into the environment.
+ // The caller retains ownership of the string; the manager will not
+ // free it. If string contains no '=', the named variable is removed
+ // (POSIX behavior). Returns 0 on success, -1 on failure.
+ int put(char *string);
+
// 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.
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index e71e3bf3e4ddf..985d8cabe944c 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -51,6 +51,20 @@ add_entrypoint_object(
libc.src.stdlib.environ_internal
)
+add_entrypoint_object(
+ putenv
+ SRCS
+ putenv.cpp
+ HDRS
+ ../putenv.h
+ DEPENDS
+ 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
+)
+
add_entrypoint_object(
unsetenv
SRCS
diff --git a/libc/src/stdlib/linux/putenv.cpp b/libc/src/stdlib/linux/putenv.cpp
new file mode 100644
index 0000000000000..3a499d6c0d1ba
--- /dev/null
+++ b/libc/src/stdlib/linux/putenv.cpp
@@ -0,0 +1,33 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 putenv function.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/putenv.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, putenv, (char *string)) {
+ LIBC_CRASH_ON_NULLPTR(string);
+
+ int result = internal::EnvironmentManager::get_instance().put(string);
+ if (result != 0)
+ libc_errno = ENOMEM;
+
+ return result;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdlib/putenv.h b/libc/src/stdlib/putenv.h
new file mode 100644
index 0000000000000..1c07c5f80a61f
--- /dev/null
+++ b/libc/src/stdlib/putenv.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 putenv function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDLIB_PUTENV_H
+#define LLVM_LIBC_SRC_STDLIB_PUTENV_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int putenv(char *string);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDLIB_PUTENV_H
diff --git a/libc/test/integration/src/stdlib/CMakeLists.txt b/libc/test/integration/src/stdlib/CMakeLists.txt
index 11b19d6b6f481..a5396785e6842 100644
--- a/libc/test/integration/src/stdlib/CMakeLists.txt
+++ b/libc/test/integration/src/stdlib/CMakeLists.txt
@@ -31,6 +31,20 @@ if(${LIBC_TARGET_OS} STREQUAL "linux")
libc.src.string.strcmp
)
+ add_integration_test(
+ putenv_test
+ SUITE
+ stdlib-integration-tests
+ SRCS
+ putenv_test.cpp
+ DEPENDS
+ libc.src.errno.errno
+ libc.src.stdlib.getenv
+ libc.src.stdlib.putenv
+ libc.src.string.strcmp
+ libc.src.unistd.environ
+ )
+
add_integration_test(
unsetenv_test
SUITE
diff --git a/libc/test/integration/src/stdlib/putenv_test.cpp b/libc/test/integration/src/stdlib/putenv_test.cpp
new file mode 100644
index 0000000000000..0781299936726
--- /dev/null
+++ b/libc/test/integration/src/stdlib/putenv_test.cpp
@@ -0,0 +1,135 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 putenv.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/getenv.h"
+#include "src/stdlib/putenv.h"
+#include "src/string/strcmp.h"
+#include "src/unistd/environ.h"
+
+#include "test/IntegrationTest/test.h"
+
+#include <errno.h>
+
+static char set_var[] = "PUTENV_TEST=hello";
+static char replace_var[] = "PUTENV_TEST=world";
+static char empty_value[] = "PUTENV_EMPTY=";
+static char special_chars[] = "PUTENV_SPECIAL=!@#$%^&*()";
+
+TEST_MAIN() {
+ // Test: Basic set
+ {
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(set_var), 0);
+ char *value = LIBC_NAMESPACE::getenv("PUTENV_TEST");
+ ASSERT_TRUE(value != nullptr);
+ ASSERT_EQ(LIBC_NAMESPACE::strcmp(value, "hello"), 0);
+ }
+
+ // Test: Overwrite existing variable
+ {
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(replace_var), 0);
+ char *value = LIBC_NAMESPACE::getenv("PUTENV_TEST");
+ ASSERT_TRUE(value != nullptr);
+ ASSERT_EQ(LIBC_NAMESPACE::strcmp(value, "world"), 0);
+ }
+
+ // Test: The pointer itself is used (not a copy)
+ {
+ // After putenv, getenv should return a pointer into the original string.
+ char *value = LIBC_NAMESPACE::getenv("PUTENV_TEST");
+ ASSERT_TRUE(value == replace_var + 12); // "PUTENV_TEST=" is 12 chars
+ }
+
+ // Test: Empty value
+ {
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(empty_value), 0);
+ char *value = LIBC_NAMESPACE::getenv("PUTENV_EMPTY");
+ ASSERT_TRUE(value != nullptr);
+ ASSERT_EQ(LIBC_NAMESPACE::strcmp(value, ""), 0);
+ }
+
+ // Test: Special characters in value
+ {
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(special_chars), 0);
+ char *value = LIBC_NAMESPACE::getenv("PUTENV_SPECIAL");
+ ASSERT_TRUE(value != nullptr);
+ ASSERT_EQ(LIBC_NAMESPACE::strcmp(value, "!@#$%^&*()"), 0);
+ }
+
+ // Test: No '=' removes the variable (POSIX behavior)
+ {
+ // First set a variable via putenv
+ static char var_to_remove[] = "REMOVE_ME=present";
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(var_to_remove), 0);
+ ASSERT_TRUE(LIBC_NAMESPACE::getenv("REMOVE_ME") != nullptr);
+
+ // Now call putenv without '=' to remove it
+ static char remove_cmd[] = "REMOVE_ME";
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(remove_cmd), 0);
+ ASSERT_TRUE(LIBC_NAMESPACE::getenv("REMOVE_ME") == nullptr);
+ }
+
+ // Test: environ is updated and contains the exact pointer
+ {
+ bool found = false;
+ for (char **env = LIBC_NAMESPACE::environ; *env != nullptr; ++env) {
+ if (*env == replace_var) {
+ found = true;
+ break;
+ }
+ }
+ ASSERT_TRUE(found);
+ }
+
+ // Test: Pathological inputs (empty names, empty string) are allowed to match
+ // glibc.
+ {
+ // Empty name with value (=value) should succeed and be found in environ.
+ static char empty_name[] = "=value";
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(empty_name), 0);
+ bool found = false;
+ for (char **env = LIBC_NAMESPACE::environ; *env != nullptr; ++env) {
+ if (*env == empty_name) {
+ found = true;
+ break;
+ }
+ }
+ ASSERT_TRUE(found);
+
+ // Just "=" should succeed and be found in environ.
+ static char just_equals[] = "=";
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(just_equals), 0);
+ found = false;
+ for (char **env = LIBC_NAMESPACE::environ; *env != nullptr; ++env) {
+ if (*env == just_equals) {
+ found = true;
+ break;
+ }
+ }
+ ASSERT_TRUE(found);
+
+ // Empty string unsets the empty name.
+ static char empty_string[] = "";
+ ASSERT_EQ(LIBC_NAMESPACE::putenv(empty_string), 0);
+ // Verify "=value" is no longer in environ (it should have been unset).
+ found = false;
+ for (char **env = LIBC_NAMESPACE::environ; *env != nullptr; ++env) {
+ if (*env == empty_name) {
+ found = true;
+ break;
+ }
+ }
+ ASSERT_FALSE(found);
+ }
+
+ return 0;
+}
More information about the libc-commits
mailing list