[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.
This commit is contained in:
Jeff Bailey 2026-07-28 15:35:04 +01:00 committed by GitHub
parent 5a650306e4
commit e362f3c846
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 279 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -130,6 +130,12 @@ public:
// 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.

View file

@ -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

View file

@ -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

25
libc/src/stdlib/putenv.h Normal file
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 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

View file

@ -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

View file

@ -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;
}