[libc-commits] [libc] [libc][stdlib] Implement setenv() with environment management infrastructure (PR #163018)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Thu May 7 09:49:10 PDT 2026


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

>From f8d98bd8eeddd653b2e80893cafea4f3f158fc3a Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Wed, 6 May 2026 20:14:37 +0100
Subject: [PATCH] [libc][stdlib] Add setenv, putenv, unsetenv

Added environment mutation functions using the EnvironmentManager
singleton. Each entrypoint validates inputs per POSIX and delegates to
a new manager method:

- set(name, value, overwrite): allocates "name=value", takes ownership.
- put(char *string): uses the caller string directly, no ownership.
  Removes the variable if no '=' is present (glibc/musl convention).
- unset(name): frees the string if we own it, compacts the array.

Ownership is tracked per-string: set marks allocated_by_us = true, put
marks it false, and unset calls delete[] only when can_free() is true.

Registered for x86_64, aarch64, and riscv.
---
 libc/config/linux/aarch64/entrypoints.txt     |   3 +
 libc/config/linux/riscv/entrypoints.txt       |   3 +
 libc/config/linux/x86_64/entrypoints.txt      |   3 +
 libc/include/stdlib.yaml                      |  20 +++
 libc/src/stdlib/CMakeLists.txt                |  22 +++
 libc/src/stdlib/environ_internal.cpp          | 113 +++++++++++++
 libc/src/stdlib/environ_internal.h            |  18 +++
 libc/src/stdlib/linux/CMakeLists.txt          |  44 +++++
 libc/src/stdlib/linux/putenv.cpp              |  33 ++++
 libc/src/stdlib/linux/setenv.cpp              |  48 ++++++
 libc/src/stdlib/linux/unsetenv.cpp            |  44 +++++
 libc/src/stdlib/putenv.h                      |  25 +++
 libc/src/stdlib/setenv.h                      |  25 +++
 libc/src/stdlib/unsetenv.h                    |  25 +++
 .../integration/src/stdlib/CMakeLists.txt     |  39 +++++
 .../integration/src/stdlib/putenv_test.cpp    |  75 +++++++++
 .../integration/src/stdlib/setenv_test.cpp    | 153 ++++++++++++++++++
 .../integration/src/stdlib/unsetenv_test.cpp  |  91 +++++++++++
 18 files changed, 784 insertions(+)
 create mode 100644 libc/src/stdlib/linux/putenv.cpp
 create mode 100644 libc/src/stdlib/linux/setenv.cpp
 create mode 100644 libc/src/stdlib/linux/unsetenv.cpp
 create mode 100644 libc/src/stdlib/putenv.h
 create mode 100644 libc/src/stdlib/setenv.h
 create mode 100644 libc/src/stdlib/unsetenv.h
 create mode 100644 libc/test/integration/src/stdlib/putenv_test.cpp
 create mode 100644 libc/test/integration/src/stdlib/setenv_test.cpp
 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 ea3f4cd8f51a0..4126ad1b54b15 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -1148,6 +1148,9 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdlib.atexit
     libc.src.stdlib.exit
     libc.src.stdlib.getenv
+    libc.src.stdlib.setenv
+    libc.src.stdlib.putenv
+    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 d8d520c6d4236..ffc9cdafa33b9 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -1277,6 +1277,9 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdlib.atexit
     libc.src.stdlib.exit
     libc.src.stdlib.getenv
+    libc.src.stdlib.setenv
+    libc.src.stdlib.putenv
+    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 dfd6064951d52..1402b48c5fb7e 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1341,6 +1341,9 @@ if(LLVM_LIBC_FULL_BUILD)
     libc.src.stdlib.atexit
     libc.src.stdlib.exit
     libc.src.stdlib.getenv
+    libc.src.stdlib.setenv
+    libc.src.stdlib.putenv
+    libc.src.stdlib.unsetenv
     libc.src.stdlib.mbstowcs
     libc.src.stdlib.mbtowc
     libc.src.stdlib.quick_exit
diff --git a/libc/include/stdlib.yaml b/libc/include/stdlib.yaml
index a751f8306be24..2e922571fe6c9 100644
--- a/libc/include/stdlib.yaml
+++ b/libc/include/stdlib.yaml
@@ -172,6 +172,12 @@ functions:
       - type: void **
       - type: size_t
       - type: size_t
+  - name: putenv
+    standards:
+      - posix
+    return_type: int
+    arguments:
+      - type: char *
   - name: qsort
     standards:
       - stdc
@@ -209,6 +215,14 @@ functions:
     return_type: void
     arguments:
       - type: unsigned int
+  - name: setenv
+    standards:
+      - posix
+    return_type: int
+    arguments:
+      - type: const char *
+      - type: const char *
+      - type: int
   - name: strfromd
     standards:
       - stdc
@@ -355,6 +369,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/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 1185f1a0fa461..86af28a1d196c 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -81,6 +81,7 @@ add_object_library(
     libc.src.__support.CPP.string_view
     libc.src.__support.macros.attributes
     libc.src.__support.macros.config
+    libc.src.string.memory_utils.inline_memcpy
 )
 
 add_entrypoint_object(
@@ -625,6 +626,27 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
   add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
 endif()
 
+add_entrypoint_object(
+  setenv
+  ALIAS
+  DEPENDS
+    .${LIBC_TARGET_OS}.setenv
+)
+
+add_entrypoint_object(
+  putenv
+  ALIAS
+  DEPENDS
+    .${LIBC_TARGET_OS}.putenv
+)
+
+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 a6b630921f9f1..cf0ef617ed509 100644
--- a/libc/src/stdlib/environ_internal.cpp
+++ b/libc/src/stdlib/environ_internal.cpp
@@ -17,6 +17,7 @@
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/alloc-checker.h"
 #include "src/__support/macros/config.h"
+#include "src/string/memory_utils/inline_memcpy.h"
 
 namespace LIBC_NAMESPACE_DECL {
 namespace internal {
@@ -171,5 +172,117 @@ bool EnvironmentManager::ensure_capacity(size_t needed) {
   return true;
 }
 
+int EnvironmentManager::set(cpp::string_view name, cpp::string_view value,
+                            bool overwrite) {
+  cpp::optional<size_t> idx = find_var(name);
+
+  // If the variable exists and we're not overwriting, do nothing.
+  if (idx && !overwrite)
+    return 0;
+
+  // Ensure we have capacity. If the variable doesn't exist, we need one
+  // more slot.
+  size_t needed = idx ? count : count + 1;
+  if (!ensure_capacity(needed))
+    return -1;
+
+  // Build the "name=value" string.
+  size_t name_len = name.size();
+  size_t value_len = value.size();
+  size_t total_len = name_len + 1 + value_len + 1; // name + '=' + value + '\0'
+
+  AllocChecker ac;
+  char *new_string = new (ac) char[total_len];
+  if (!ac)
+    return -1;
+
+  inline_memcpy(new_string, name.data(), name_len);
+  new_string[name_len] = '=';
+  inline_memcpy(new_string + name_len + 1, value.data(), value_len);
+  new_string[name_len + 1 + value_len] = '\0';
+
+  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] = new_string;
+    ownership[*idx].allocated_by_us = true;
+  } else {
+    // Add new variable at the end.
+    env_array[count] = new_string;
+    ownership[count].allocated_by_us = true;
+    count++;
+    env_array[count] = nullptr; // Maintain null terminator.
+  }
+
+  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;
+}
+
+int EnvironmentManager::put(char *string) {
+  cpp::string_view sv(string);
+  size_t eq_pos = sv.find_first_of('=');
+
+  // No '=' found: treat as unset (glibc/musl convention).
+  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 0ab75e200b968..c315f2d8626e0 100644
--- a/libc/src/stdlib/environ_internal.h
+++ b/libc/src/stdlib/environ_internal.h
@@ -122,6 +122,24 @@ class EnvironmentManager {
   // Look up a variable by name. Returns a pointer to the value string
   // (after the '='), or nullptr if not found.
   char *get(cpp::string_view name);
+
+  // Set or update an environment variable. Builds a "name=value" string,
+  // manages ownership, and updates the environ array. If `overwrite` is
+  // false and the variable already exists, does nothing and returns 0.
+  // 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);
+
+  // 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
+  // (glibc/musl convention). 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.
+  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 8a4b2bab1c53d..c782548f2546a 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -8,3 +8,47 @@ add_header_library(
     libc.src.signal.linux.__restore
     libc.src.signal.linux.signal_utils
 )
+
+add_entrypoint_object(
+  setenv
+  SRCS
+    setenv.cpp
+  HDRS
+    ../setenv.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
+)
+
+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
+    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/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/linux/setenv.cpp b/libc/src/stdlib/linux/setenv.cpp
new file mode 100644
index 0000000000000..f9c2075fb7787
--- /dev/null
+++ b/libc/src/stdlib/linux/setenv.cpp
@@ -0,0 +1,48 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 setenv function.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/setenv.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, setenv,
+                   (const char *name, const char *value, int overwrite)) {
+  // Passing nullptr for either argument is undefined behavior per POSIX,
+  // so crash rather than returning an error.
+  LIBC_CRASH_ON_NULLPTR(name);
+  LIBC_CRASH_ON_NULLPTR(value);
+
+  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().set(name_view, value,
+                                                       overwrite != 0);
+  if (result != 0)
+    libc_errno = ENOMEM;
+
+  return result;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdlib/linux/unsetenv.cpp b/libc/src/stdlib/linux/unsetenv.cpp
new file mode 100644
index 0000000000000..129ea992d4c59
--- /dev/null
+++ b/libc/src/stdlib/linux/unsetenv.cpp
@@ -0,0 +1,44 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/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/src/stdlib/setenv.h b/libc/src/stdlib/setenv.h
new file mode 100644
index 0000000000000..365d768cdd668
--- /dev/null
+++ b/libc/src/stdlib/setenv.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 setenv function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDLIB_SETENV_H
+#define LLVM_LIBC_SRC_STDLIB_SETENV_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int setenv(const char *name, const char *value, int overwrite);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDLIB_SETENV_H
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 cf39b7ceb2f87..3c7285dc76c7f 100644
--- a/libc/test/integration/src/stdlib/CMakeLists.txt
+++ b/libc/test/integration/src/stdlib/CMakeLists.txt
@@ -17,6 +17,45 @@ add_integration_test(
     GERMANY=Berlin
 )
 
+if(${LIBC_TARGET_OS} STREQUAL "linux")
+  add_integration_test(
+    setenv_test
+    SUITE
+      stdlib-integration-tests
+    SRCS
+      setenv_test.cpp
+    DEPENDS
+      libc.src.stdlib.getenv
+      libc.src.stdlib.setenv
+      libc.src.string.strcmp
+  )
+
+  add_integration_test(
+    putenv_test
+    SUITE
+      stdlib-integration-tests
+    SRCS
+      putenv_test.cpp
+    DEPENDS
+      libc.src.stdlib.getenv
+      libc.src.stdlib.putenv
+      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
+  )
+endif()
+
 if(${LIBC_TARGET_OS} STREQUAL "linux")
   add_integration_test(
     abort_test
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..ad9305e3f80cc
--- /dev/null
+++ b/libc/test/integration/src/stdlib/putenv_test.cpp
@@ -0,0 +1,75 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/getenv.h"
+#include "src/stdlib/putenv.h"
+#include "src/string/strcmp.h"
+
+#include "test/IntegrationTest/test.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([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
+          [[maybe_unused]] char **envp) {
+  // 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 (glibc/musl convention)
+  {
+    // 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);
+  }
+
+  return 0;
+}
diff --git a/libc/test/integration/src/stdlib/setenv_test.cpp b/libc/test/integration/src/stdlib/setenv_test.cpp
new file mode 100644
index 0000000000000..4f9cc683433ab
--- /dev/null
+++ b/libc/test/integration/src/stdlib/setenv_test.cpp
@@ -0,0 +1,153 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/getenv.h"
+#include "src/stdlib/setenv.h"
+#include "src/string/strcmp.h"
+
+#include "test/IntegrationTest/test.h"
+
+#include <errno.h>
+
+TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
+          [[maybe_unused]] char **envp) {
+  // Test: Basic
+  {
+    // Set a simple environment variable
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("SETENV_TEST_VAR", "test_value", 1), 0);
+
+    // Verify it was set
+    char *value = LIBC_NAMESPACE::getenv("SETENV_TEST_VAR");
+    ASSERT_TRUE(value != nullptr);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(value, "test_value"), 0);
+  }
+
+  // Test: OverwriteExisting
+  {
+    // Set initial value
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("OVERWRITE_VAR", "original", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("OVERWRITE_VAR"),
+                                     "original"),
+              0);
+
+    // Overwrite with new value (overwrite = 1)
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("OVERWRITE_VAR", "replaced", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("OVERWRITE_VAR"),
+                                     "replaced"),
+              0);
+  }
+
+  // Test: NoOverwriteFlag
+  {
+    // Set initial value
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("NO_OVERWRITE_VAR", "original", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("NO_OVERWRITE_VAR"),
+                                     "original"),
+              0);
+
+    // Try to set with overwrite = 0 (should not change)
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("NO_OVERWRITE_VAR", "ignored", 0), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("NO_OVERWRITE_VAR"),
+                                     "original"),
+              0);
+
+    // Verify it still works with overwrite = 1
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("NO_OVERWRITE_VAR", "changed", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("NO_OVERWRITE_VAR"),
+                                     "changed"),
+              0);
+  }
+
+  // Note: passing nullptr for name or value is undefined behavior per POSIX.
+  // The implementation uses LIBC_CRASH_ON_NULLPTR for both, so there is no
+  // test for that case here.
+
+  // Test: EmptyName
+  {
+    errno = 0;
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("", "value", 1), -1);
+    ASSERT_ERRNO_EQ(EINVAL);
+  }
+
+  // Test: NameWithEquals
+  {
+    errno = 0;
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("BAD=NAME", "value", 1), -1);
+    ASSERT_ERRNO_EQ(EINVAL);
+  }
+
+  // Test: EmptyValue
+  {
+    // Empty value is valid - just means variable is set to empty string
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("EMPTY_VALUE_VAR", "", 1), 0);
+
+    char *value = LIBC_NAMESPACE::getenv("EMPTY_VALUE_VAR");
+    ASSERT_TRUE(value != nullptr);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(value, ""), 0);
+  }
+
+  // Test: MultipleVariables
+  {
+    // Set multiple different variables
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("VAR1", "value1", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("VAR2", "value2", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("VAR3", "value3", 1), 0);
+
+    // Verify all are set correctly
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("VAR1"), "value1"),
+              0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("VAR2"), "value2"),
+              0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("VAR3"), "value3"),
+              0);
+  }
+
+  // Test: LongValues
+  {
+    // Test with longer strings
+    const char *long_name = "LONG_VAR_NAME_FOR_TESTING";
+    const char *long_value = "This is a fairly long value string to test that "
+                             "setenv handles longer strings correctly without "
+                             "any memory issues or truncation problems";
+
+    ASSERT_EQ(LIBC_NAMESPACE::setenv(long_name, long_value, 1), 0);
+    ASSERT_EQ(
+        LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv(long_name), long_value),
+        0);
+  }
+
+  // Test: SpecialCharacters
+  {
+    // Test with special characters in value (but not in name)
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("SPECIAL_CHARS", "!@#$%^&*()", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("SPECIAL_CHARS"),
+                                     "!@#$%^&*()"),
+              0);
+  }
+
+  // Test: ReplaceMultipleTimes
+  {
+    // Replace the same variable multiple times
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("MULTI_REPLACE", "value1", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("MULTI_REPLACE"),
+                                     "value1"),
+              0);
+
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("MULTI_REPLACE", "value2", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("MULTI_REPLACE"),
+                                     "value2"),
+              0);
+
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("MULTI_REPLACE", "value3", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("MULTI_REPLACE"),
+                                     "value3"),
+              0);
+  }
+
+  return 0;
+}
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..b4d6aa0ce6c11
--- /dev/null
+++ b/libc/test/integration/src/stdlib/unsetenv_test.cpp
@@ -0,0 +1,91 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/getenv.h"
+#include "src/stdlib/setenv.h"
+#include "src/stdlib/unsetenv.h"
+#include "src/string/strcmp.h"
+
+#include "test/IntegrationTest/test.h"
+
+#include <errno.h>
+
+TEST_MAIN([[maybe_unused]] int argc, [[maybe_unused]] char **argv,
+          [[maybe_unused]] char **envp) {
+  // Test: Remove a variable set by setenv
+  {
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("UNSET_VAR", "value", 1), 0);
+    ASSERT_TRUE(LIBC_NAMESPACE::getenv("UNSET_VAR") != nullptr);
+
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv("UNSET_VAR"), 0);
+    ASSERT_TRUE(LIBC_NAMESPACE::getenv("UNSET_VAR") == nullptr);
+  }
+
+  // Test: Unset non-existent variable succeeds
+  {
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv("DOES_NOT_EXIST"), 0);
+  }
+
+  // Test: Empty name returns EINVAL
+  {
+    errno = 0;
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv(""), -1);
+    ASSERT_ERRNO_EQ(EINVAL);
+  }
+
+  // Test: Name with '=' returns EINVAL
+  {
+    errno = 0;
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv("BAD=NAME"), -1);
+    ASSERT_ERRNO_EQ(EINVAL);
+  }
+
+  // Test: Unset then re-set
+  {
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("REUSE_VAR", "first", 1), 0);
+    ASSERT_EQ(
+        LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("REUSE_VAR"), "first"),
+        0);
+
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv("REUSE_VAR"), 0);
+    ASSERT_TRUE(LIBC_NAMESPACE::getenv("REUSE_VAR") == nullptr);
+
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("REUSE_VAR", "second", 1), 0);
+    ASSERT_EQ(
+        LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("REUSE_VAR"), "second"),
+        0);
+  }
+
+  // Test: Unset multiple variables
+  {
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("MULTI_A", "a", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("MULTI_B", "b", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("MULTI_C", "c", 1), 0);
+
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv("MULTI_B"), 0);
+
+    ASSERT_TRUE(LIBC_NAMESPACE::getenv("MULTI_A") != nullptr);
+    ASSERT_TRUE(LIBC_NAMESPACE::getenv("MULTI_B") == nullptr);
+    ASSERT_TRUE(LIBC_NAMESPACE::getenv("MULTI_C") != nullptr);
+
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("MULTI_A"), "a"),
+              0);
+    ASSERT_EQ(LIBC_NAMESPACE::strcmp(LIBC_NAMESPACE::getenv("MULTI_C"), "c"),
+              0);
+  }
+
+  // Test: Unset same variable twice is harmless
+  {
+    ASSERT_EQ(LIBC_NAMESPACE::setenv("DOUBLE_UNSET", "val", 1), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv("DOUBLE_UNSET"), 0);
+    ASSERT_EQ(LIBC_NAMESPACE::unsetenv("DOUBLE_UNSET"), 0);
+    ASSERT_TRUE(LIBC_NAMESPACE::getenv("DOUBLE_UNSET") == nullptr);
+  }
+
+  return 0;
+}



More information about the libc-commits mailing list