[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