towerops-agent/native/snmp_helper.c
Graham McIntire a9bf0b0f8b
Switch from Alpine (musl) to Debian slim (glibc) to fix SNMP SIGSEGV
musl libc has weak fork-safety guarantees in multi-threaded processes.
When Tokio's runtime is running and we fork() for SNMP operations, musl's
internal state can be inconsistent in the child, causing SIGSEGV on every
SNMP walk to Ubiquiti devices. glibc handles this via pthread_atfork.

- Dockerfile: Alpine → rust:1.93-bookworm build + debian:12-slim runtime
- CI: Remove ensure-netsnmp-base/manifest jobs, simplify build deps
- Delete Dockerfile.netsnmp and netsnmp-base.yml workflow
- Add NULL pointer guards in snmp_helper.c for fork-safety
- Add SNMP exception type handling (NoSuchObject/Instance/EndOfMibView)
- Add crash reproduction tests
2026-02-11 08:41:06 -06:00

953 lines
30 KiB
C

#include "snmp_helper.h"
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdlib.h>
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
static void init_snmp_once(void) {
// Initialize the SNMP library
init_snmp("towerops-agent");
// Configure to output numeric OIDs only (no MIB names)
// This ensures OIDs are in format "1.3.6.1.2.1.1.1.0" not "SNMPv2-MIB::sysDescr.0"
netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT,
NETSNMP_OID_OUTPUT_NUMERIC);
}
int snmp_init_library(void) {
pthread_once(&init_once, init_snmp_once);
return 0;
}
void* snmp_open_session(
const char* ip_address,
uint16_t port,
const char* community,
int version,
int64_t timeout_us,
int retries,
const snmp_v3_config_t* v3_config,
char* error_buf,
size_t error_buf_len
) {
struct snmp_session session, *sess_handle;
// Ensure library is initialized
snmp_init_library();
// Initialize session structure
snmp_sess_init(&session);
// Set peer address with port (e.g., "192.168.1.1:161")
// This is the modern way - remote_port field is deprecated
char peername[256];
snprintf(peername, sizeof(peername), "%s:%u", ip_address, port);
session.peername = strdup(peername);
if (!session.peername) {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Failed to allocate memory for peer address");
}
return NULL;
}
// Set SNMP version
switch (version) {
case 1:
session.version = SNMP_VERSION_1;
break;
case 2:
session.version = SNMP_VERSION_2c;
break;
case 3:
session.version = SNMP_VERSION_3;
break;
default:
free(session.peername);
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Unsupported SNMP version: %d", version);
}
return NULL;
}
// Configure version-specific parameters
if (version == 3) {
// SNMPv3 configuration
if (!v3_config || !v3_config->username) {
free(session.peername);
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "SNMPv3 requires username");
}
return NULL;
}
// Set security name (username)
session.securityName = strdup(v3_config->username);
session.securityNameLen = strlen(v3_config->username);
// Set security level
if (v3_config->security_level) {
if (strcmp(v3_config->security_level, "authPriv") == 0) {
session.securityLevel = SNMP_SEC_LEVEL_AUTHPRIV;
} else if (strcmp(v3_config->security_level, "authNoPriv") == 0) {
session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV;
} else {
session.securityLevel = SNMP_SEC_LEVEL_NOAUTH;
}
} else {
session.securityLevel = SNMP_SEC_LEVEL_NOAUTH;
}
// Set authentication protocol and password
if (session.securityLevel >= SNMP_SEC_LEVEL_AUTHNOPRIV) {
if (v3_config->auth_password) {
session.securityAuthProto = usmHMACMD5AuthProtocol;
session.securityAuthProtoLen = USM_AUTH_PROTO_MD5_LEN;
if (v3_config->auth_protocol) {
if (strcmp(v3_config->auth_protocol, "SHA") == 0) {
session.securityAuthProto = usmHMACSHA1AuthProtocol;
session.securityAuthProtoLen = USM_AUTH_PROTO_SHA_LEN;
}
}
session.securityAuthKeyLen = USM_AUTH_KU_LEN;
if (generate_Ku(session.securityAuthProto,
session.securityAuthProtoLen,
(u_char*)v3_config->auth_password,
strlen(v3_config->auth_password),
session.securityAuthKey,
&session.securityAuthKeyLen) != SNMPERR_SUCCESS) {
free(session.peername);
free((void*)session.securityName);
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Failed to generate auth key");
}
return NULL;
}
}
}
// Set privacy protocol and password
if (session.securityLevel >= SNMP_SEC_LEVEL_AUTHPRIV) {
if (v3_config->priv_password) {
session.securityPrivProto = usmDESPrivProtocol;
session.securityPrivProtoLen = USM_PRIV_PROTO_DES_LEN;
if (v3_config->priv_protocol) {
if (strcmp(v3_config->priv_protocol, "AES") == 0) {
session.securityPrivProto = usmAESPrivProtocol;
session.securityPrivProtoLen = USM_PRIV_PROTO_AES_LEN;
}
}
session.securityPrivKeyLen = USM_PRIV_KU_LEN;
if (generate_Ku(session.securityAuthProto,
session.securityAuthProtoLen,
(u_char*)v3_config->priv_password,
strlen(v3_config->priv_password),
session.securityPrivKey,
&session.securityPrivKeyLen) != SNMPERR_SUCCESS) {
free(session.peername);
free((void*)session.securityName);
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Failed to generate priv key");
}
return NULL;
}
}
}
} else {
// v1/v2c: Set community string
if (community && community[0]) {
session.community = (u_char*)strdup(community);
if (!session.community) {
free(session.peername);
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Failed to allocate memory for community string");
}
return NULL;
}
session.community_len = strlen(community);
}
}
// Set timeout and retries
session.timeout = timeout_us;
session.retries = retries;
// Open the session
sess_handle = snmp_sess_open(&session);
// Clean up temporary allocations
free(session.peername);
if (session.community) {
// Zero out community string before freeing
memset((void*)session.community, 0, session.community_len);
free((void*)session.community);
}
if (session.securityName) {
free((void*)session.securityName);
}
// Check for errors
if (!sess_handle) {
if (error_buf && error_buf_len > 0) {
// Get error message from library
int liberr, syserr;
char *errstr;
snmp_error(&session, &liberr, &syserr, &errstr);
snprintf(error_buf, error_buf_len, "%s", errstr);
free(errstr);
}
return NULL;
}
return sess_handle;
}
void snmp_close_session(void* sess_handle) {
if (sess_handle) {
snmp_sess_close(sess_handle);
}
}
int snmp_get(
void* sess_handle,
const char* oid_str,
void* value_buf,
size_t value_buf_len,
int* value_type,
char* error_buf,
size_t error_buf_len
) {
if (!sess_handle || !oid_str || !value_buf || !value_type) {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Invalid parameters");
}
return -1;
}
oid anOID[MAX_OID_LEN];
size_t anOID_len = MAX_OID_LEN;
// Parse OID string
if (!read_objid(oid_str, anOID, &anOID_len)) {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Failed to parse OID: %s", oid_str);
}
return -1;
}
// Create GET PDU
struct snmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GET);
if (!pdu) {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Failed to create PDU");
}
return -1;
}
// Add OID to PDU
snmp_add_null_var(pdu, anOID, anOID_len);
// Send request
struct snmp_pdu *response = NULL;
int status = snmp_sess_synch_response(sess_handle, pdu, &response);
if (status != STAT_SUCCESS || !response) {
if (error_buf && error_buf_len > 0) {
if (status == STAT_TIMEOUT) {
snprintf(error_buf, error_buf_len, "Request timeout");
} else {
snprintf(error_buf, error_buf_len, "Request failed");
}
}
if (response) {
snmp_free_pdu(response);
}
return -1;
}
// Extract value from response
int result = -1;
if (response->variables) {
struct variable_list *var = response->variables;
*value_type = var->type;
// Handle SNMP exception types (NoSuchObject, NoSuchInstance, EndOfMibView)
if (var->type == SNMP_NOSUCHOBJECT ||
var->type == SNMP_NOSUCHINSTANCE ||
var->type == SNMP_ENDOFMIBVIEW) {
if (error_buf && error_buf_len > 0) {
const char *label = var->type == SNMP_NOSUCHOBJECT ? "noSuchObject" :
var->type == SNMP_NOSUCHINSTANCE ? "noSuchInstance" :
"endOfMibView";
snprintf(error_buf, error_buf_len, "%s", label);
}
snmp_free_pdu(response);
return -1;
}
switch (var->type) {
case ASN_OCTET_STR:
case ASN_OPAQUE:
case ASN_IPADDRESS:
if (var->val.string && var->val_len > 0 &&
var->val_len <= value_buf_len) {
memcpy(value_buf, var->val.string, var->val_len);
result = (int)var->val_len;
} else if (!var->val.string || var->val_len == 0) {
result = 0; // Empty string
} else {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Buffer too small");
}
}
break;
case ASN_INTEGER:
case ASN_COUNTER:
case ASN_GAUGE:
case ASN_TIMETICKS:
case ASN_UINTEGER:
if (var->val.integer && sizeof(long) <= value_buf_len) {
*((long*)value_buf) = *var->val.integer;
result = sizeof(long);
}
break;
case ASN_COUNTER64:
if (var->val.counter64 &&
sizeof(struct counter64) <= value_buf_len) {
memcpy(value_buf, var->val.counter64, sizeof(struct counter64));
result = sizeof(struct counter64);
}
break;
case ASN_OBJECT_ID:
if (var->val.objid && var->val_len > 0) {
char oid_buf[256];
snprint_objid(oid_buf, sizeof(oid_buf), var->val.objid,
var->val_len / sizeof(oid));
size_t oid_str_len = strlen(oid_buf);
if (oid_str_len <= value_buf_len) {
memcpy(value_buf, oid_buf, oid_str_len);
result = (int)oid_str_len;
} else {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len,
"Buffer too small for OID string");
}
}
}
break;
case ASN_NULL:
// NULL values are valid but contain no data
result = 0;
break;
default:
// Unknown type
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len,
"Unsupported type: %d", var->type);
}
break;
}
}
snmp_free_pdu(response);
return result;
}
int snmp_walk(
void* sess_handle,
const char* oid_str,
snmp_walk_result_t* results,
size_t max_results,
size_t* num_results,
char* error_buf,
size_t error_buf_len
) {
if (!sess_handle || !oid_str || !results || !num_results) {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Invalid parameters");
}
return -1;
}
oid root[MAX_OID_LEN];
size_t rootlen = MAX_OID_LEN;
// Parse starting OID
if (!read_objid(oid_str, root, &rootlen)) {
if (error_buf && error_buf_len > 0) {
snprintf(error_buf, error_buf_len, "Failed to parse OID: %s", oid_str);
}
return -1;
}
oid name[MAX_OID_LEN];
size_t name_length = rootlen;
memcpy(name, root, rootlen * sizeof(oid));
*num_results = 0;
int running = 1;
while (running && *num_results < max_results) {
// Create GETNEXT PDU
struct snmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GETNEXT);
if (!pdu) {
break;
}
snmp_add_null_var(pdu, name, name_length);
// Send request
struct snmp_pdu *response = NULL;
int status = snmp_sess_synch_response(sess_handle, pdu, &response);
if (status != STAT_SUCCESS || !response || !response->variables) {
if (response) {
snmp_free_pdu(response);
}
break;
}
struct variable_list *var = response->variables;
// Check if we've walked past the root OID
if (var->name_length < rootlen ||
snmp_oid_ncompare(var->name, var->name_length, root, rootlen, rootlen) != 0) {
snmp_free_pdu(response);
break;
}
// Handle SNMP exception types that indicate end-of-data or missing values.
// These have type 0x80 (NoSuchObject), 0x81 (NoSuchInstance),
// 0x82 (EndOfMibView) and their val pointers may be NULL.
if (var->type == SNMP_NOSUCHOBJECT ||
var->type == SNMP_NOSUCHINSTANCE) {
// Object/instance doesn't exist at this index - skip and continue walk
// Update OID for next iteration
if (var->name_length <= MAX_OID_LEN) {
memcpy(name, var->name, var->name_length * sizeof(oid));
name_length = var->name_length;
} else {
running = 0;
}
snmp_free_pdu(response);
continue;
}
if (var->type == SNMP_ENDOFMIBVIEW) {
// No more data available - terminate the walk
snmp_free_pdu(response);
break;
}
// Store result
snmp_walk_result_t *res = &results[*num_results];
// Convert OID to string
snprint_objid(res->oid, sizeof(res->oid), var->name, var->name_length);
// Store value - check for NULL val pointers before every dereference.
// After fork() from a multi-threaded process, net-snmp's internal state
// can be inconsistent, potentially leaving val pointers NULL even for
// standard types.
res->value_type = var->type;
res->value_len = 0;
switch (var->type) {
case ASN_OCTET_STR:
case ASN_OPAQUE:
case ASN_IPADDRESS:
if (var->val.string && var->val_len > 0 &&
var->val_len <= sizeof(res->value)) {
memcpy(res->value, var->val.string, var->val_len);
res->value_len = var->val_len;
}
break;
case ASN_OBJECT_ID:
if (var->val.objid && var->val_len > 0) {
char oid_buf[256];
snprint_objid(oid_buf, sizeof(oid_buf), var->val.objid,
var->val_len / sizeof(oid));
size_t oid_str_len = strlen(oid_buf);
if (oid_str_len < sizeof(res->value)) {
memcpy(res->value, oid_buf, oid_str_len);
res->value_len = oid_str_len;
}
}
break;
case ASN_INTEGER:
case ASN_COUNTER:
case ASN_GAUGE:
case ASN_TIMETICKS:
case ASN_UINTEGER:
if (var->val.integer && sizeof(long) <= sizeof(res->value)) {
*((long*)res->value) = *var->val.integer;
res->value_len = sizeof(long);
}
break;
case ASN_COUNTER64:
if (var->val.counter64 &&
sizeof(struct counter64) <= sizeof(res->value)) {
memcpy(res->value, var->val.counter64, sizeof(struct counter64));
res->value_len = sizeof(struct counter64);
}
break;
case ASN_NULL:
// NULL values are valid but contain no data - skip
break;
default:
// Unknown or unsupported type - skip silently
break;
}
if (res->value_len > 0) {
(*num_results)++;
}
// Update OID for next iteration
if (var->name_length <= MAX_OID_LEN) {
memcpy(name, var->name, var->name_length * sizeof(oid));
name_length = var->name_length;
} else {
running = 0;
}
snmp_free_pdu(response);
}
return 0;
}
/* --- Process-isolated (fork-based) operations --- */
/**
* Write exactly `len` bytes to fd, retrying on EINTR.
* Returns 0 on success, -1 on error.
*/
static int write_full(int fd, const void* buf, size_t len) {
const uint8_t* p = (const uint8_t*)buf;
size_t remaining = len;
while (remaining > 0) {
ssize_t n = write(fd, p, remaining);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
p += n;
remaining -= (size_t)n;
}
return 0;
}
/**
* Read exactly `len` bytes from fd, retrying on EINTR.
* Returns 0 on success, -1 on error/EOF.
*/
static int read_full(int fd, void* buf, size_t len) {
uint8_t* p = (uint8_t*)buf;
size_t remaining = len;
while (remaining > 0) {
ssize_t n = read(fd, p, remaining);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
if (n == 0) return -1; /* unexpected EOF */
p += n;
remaining -= (size_t)n;
}
return 0;
}
/**
* Reset signal handlers to defaults in the child process.
* This ensures any crash handler installed by the parent doesn't interfere.
*/
static void child_reset_signals(void) {
signal(SIGSEGV, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
}
/*
* Serialize fork operations to avoid issues with concurrent forks
* in multi-threaded processes. On macOS, concurrent fork() from
* multiple threads can trigger Objective-C runtime crashes (SIGKILL).
* On Linux this is still beneficial as it prevents resource exhaustion.
*/
static pthread_mutex_t fork_mutex = PTHREAD_MUTEX_INITIALIZER;
#ifdef __APPLE__
/*
* On macOS, the Objective-C runtime kills forked children from
* multi-threaded parents by default. Our children never use Objective-C
* and _exit() after SNMP work, so this is safe to disable.
* Set before main() to ensure it's in place before any threads start.
*/
__attribute__((constructor))
static void disable_objc_fork_safety(void) {
setenv("OBJC_DISABLE_INITIALIZE_FORK_SAFETY", "YES", 0);
}
#endif
void snmp_get_isolated(
const char* ip_address,
uint16_t port,
const char* community,
int version,
int64_t timeout_us,
int retries,
const snmp_v3_config_t* v3_config,
const char* oid_str,
snmp_isolated_get_result_t* result
) {
/* Initialize result to error state */
memset(result, 0, sizeof(*result));
result->status = -1;
pthread_mutex_lock(&fork_mutex);
int pipefd[2];
if (pipe(pipefd) != 0) {
snprintf(result->error_buf, sizeof(result->error_buf),
"pipe() failed: %s", strerror(errno));
pthread_mutex_unlock(&fork_mutex);
return;
}
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
snprintf(result->error_buf, sizeof(result->error_buf),
"fork() failed: %s", strerror(errno));
pthread_mutex_unlock(&fork_mutex);
return;
}
if (pid == 0) {
/* === CHILD PROCESS === */
close(pipefd[0]); /* close read end */
child_reset_signals();
alarm(60); /* watchdog: kill child if stuck */
/* Disable MIB loading to prevent crashes from missing/corrupt MIB files.
* Set env vars BEFORE init_snmp() runs (via snmp_open_session below).
* Do NOT call init_snmp() directly here - snmp_open_session() calls
* snmp_init_library() which uses pthread_once to initialize exactly once. */
setenv("MIBS", "", 1);
setenv("MIBDIRS", "", 1);
snmp_isolated_get_result_t child_result;
memset(&child_result, 0, sizeof(child_result));
child_result.status = -1;
/* Open session */
char error_buf[512] = {0};
void* sess = snmp_open_session(ip_address, port, community, version,
timeout_us, retries, v3_config,
error_buf, sizeof(error_buf));
if (!sess) {
snprintf(child_result.error_buf, sizeof(child_result.error_buf),
"%s", error_buf);
write_full(pipefd[1], &child_result, sizeof(child_result));
close(pipefd[1]);
_exit(1);
}
/* Perform GET */
int value_type = 0;
int ret = snmp_get(sess, oid_str,
child_result.value_buf, sizeof(child_result.value_buf),
&value_type, child_result.error_buf,
sizeof(child_result.error_buf));
snmp_close_session(sess);
child_result.status = ret;
child_result.value_type = value_type;
write_full(pipefd[1], &child_result, sizeof(child_result));
close(pipefd[1]);
_exit(0);
}
/* === PARENT PROCESS === */
close(pipefd[1]); /* close write end */
/* Unlock after fork so other threads can proceed */
pthread_mutex_unlock(&fork_mutex);
/* Try to read the result from the child */
snmp_isolated_get_result_t pipe_result;
memset(&pipe_result, 0, sizeof(pipe_result));
int read_ok = read_full(pipefd[0], &pipe_result, sizeof(pipe_result));
close(pipefd[0]);
/* Wait for child to exit */
int wstatus = 0;
pid_t waited;
do {
waited = waitpid(pid, &wstatus, 0);
} while (waited < 0 && errno == EINTR);
if (waited < 0) {
snprintf(result->error_buf, sizeof(result->error_buf),
"waitpid() failed: %s", strerror(errno));
result->status = -1;
return;
}
if (WIFSIGNALED(wstatus)) {
/* Child was killed by a signal (crash) */
result->status = -2;
result->child_signal = WTERMSIG(wstatus);
snprintf(result->error_buf, sizeof(result->error_buf),
"SNMP child process killed by signal %d", result->child_signal);
return;
}
if (read_ok != 0) {
/* Could not read from pipe but child exited normally - unexpected */
result->status = -1;
snprintf(result->error_buf, sizeof(result->error_buf),
"Failed to read result from child (exit code %d)",
WEXITSTATUS(wstatus));
return;
}
/* Successfully read result from child */
memcpy(result, &pipe_result, sizeof(*result));
}
void snmp_walk_isolated(
const char* ip_address,
uint16_t port,
const char* community,
int version,
int64_t timeout_us,
int retries,
const snmp_v3_config_t* v3_config,
const char* oid_str,
snmp_isolated_walk_header_t* header,
snmp_walk_result_t* results,
size_t max_results
) {
/* Initialize header to error state */
memset(header, 0, sizeof(*header));
header->status = -1;
pthread_mutex_lock(&fork_mutex);
int pipefd[2];
if (pipe(pipefd) != 0) {
snprintf(header->error_buf, sizeof(header->error_buf),
"pipe() failed: %s", strerror(errno));
pthread_mutex_unlock(&fork_mutex);
return;
}
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
snprintf(header->error_buf, sizeof(header->error_buf),
"fork() failed: %s", strerror(errno));
pthread_mutex_unlock(&fork_mutex);
return;
}
if (pid == 0) {
/* === CHILD PROCESS === */
close(pipefd[0]); /* close read end */
child_reset_signals();
alarm(60); /* watchdog */
/* Disable MIB loading to prevent crashes from missing/corrupt MIB files.
* Set env vars BEFORE init_snmp() runs (via snmp_open_session below).
* Do NOT call init_snmp() directly here - snmp_open_session() calls
* snmp_init_library() which uses pthread_once to initialize exactly once. */
setenv("MIBS", "", 1);
setenv("MIBDIRS", "", 1);
snmp_isolated_walk_header_t child_header;
memset(&child_header, 0, sizeof(child_header));
child_header.status = -1;
/* Open session */
char error_buf[512] = {0};
void* sess = snmp_open_session(ip_address, port, community, version,
timeout_us, retries, v3_config,
error_buf, sizeof(error_buf));
if (!sess) {
snprintf(child_header.error_buf, sizeof(child_header.error_buf),
"%s", error_buf);
write_full(pipefd[1], &child_header, sizeof(child_header));
close(pipefd[1]);
_exit(1);
}
/* Allocate results buffer in child */
snmp_walk_result_t* child_results = (snmp_walk_result_t*)calloc(
max_results, sizeof(snmp_walk_result_t));
if (!child_results) {
snprintf(child_header.error_buf, sizeof(child_header.error_buf),
"Failed to allocate walk results buffer");
snmp_close_session(sess);
write_full(pipefd[1], &child_header, sizeof(child_header));
close(pipefd[1]);
_exit(1);
}
/* Perform WALK */
size_t num_results = 0;
int ret = snmp_walk(sess, oid_str, child_results, max_results,
&num_results, child_header.error_buf,
sizeof(child_header.error_buf));
snmp_close_session(sess);
child_header.status = ret;
child_header.num_results = (uint32_t)num_results;
/* Write header first */
write_full(pipefd[1], &child_header, sizeof(child_header));
/* Write each result individually (each < PIPE_BUF) */
for (size_t i = 0; i < num_results; i++) {
write_full(pipefd[1], &child_results[i], sizeof(snmp_walk_result_t));
}
free(child_results);
close(pipefd[1]);
_exit(0);
}
/* === PARENT PROCESS === */
close(pipefd[1]); /* close write end */
/* Unlock after fork so other threads can proceed */
pthread_mutex_unlock(&fork_mutex);
/* Read header from child */
snmp_isolated_walk_header_t pipe_header;
memset(&pipe_header, 0, sizeof(pipe_header));
int read_ok = read_full(pipefd[0], &pipe_header, sizeof(pipe_header));
uint32_t results_read = 0;
if (read_ok == 0 && pipe_header.status >= 0 && pipe_header.num_results > 0) {
/* Read individual results, capping at max_results */
uint32_t to_read = pipe_header.num_results;
if (to_read > (uint32_t)max_results) {
to_read = (uint32_t)max_results;
}
for (uint32_t i = 0; i < to_read; i++) {
if (read_full(pipefd[0], &results[i], sizeof(snmp_walk_result_t)) != 0) {
break;
}
results_read++;
}
}
close(pipefd[0]);
/* Wait for child to exit */
int wstatus = 0;
pid_t waited;
do {
waited = waitpid(pid, &wstatus, 0);
} while (waited < 0 && errno == EINTR);
if (waited < 0) {
snprintf(header->error_buf, sizeof(header->error_buf),
"waitpid() failed: %s", strerror(errno));
header->status = -1;
return;
}
if (WIFSIGNALED(wstatus)) {
/* Child was killed by a signal (crash) */
header->status = -2;
header->child_signal = WTERMSIG(wstatus);
snprintf(header->error_buf, sizeof(header->error_buf),
"SNMP child process killed by signal %d", header->child_signal);
return;
}
if (read_ok != 0) {
header->status = -1;
snprintf(header->error_buf, sizeof(header->error_buf),
"Failed to read header from child (exit code %d)",
WEXITSTATUS(wstatus));
return;
}
/* Successfully read from child */
memcpy(header, &pipe_header, sizeof(*header));
header->num_results = results_read;
}
#ifdef SNMP_HELPER_TEST
int snmp_test_crash_in_child(int* child_signal) {
if (!child_signal) return -1;
*child_signal = 0;
int pipefd[2];
if (pipe(pipefd) != 0) return -1;
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
if (pid == 0) {
/* Child: close pipe ends and deliberately crash */
close(pipefd[0]);
close(pipefd[1]);
child_reset_signals();
/* Trigger SIGSEGV by writing to a null pointer */
volatile int* null_ptr = NULL;
*null_ptr = 42;
_exit(99); /* should not reach here */
}
/* Parent */
close(pipefd[0]);
close(pipefd[1]);
int wstatus = 0;
pid_t waited;
do {
waited = waitpid(pid, &wstatus, 0);
} while (waited < 0 && errno == EINTR);
if (waited < 0) return -1;
if (WIFSIGNALED(wstatus)) {
*child_signal = WTERMSIG(wstatus);
return 0;
}
return -1; /* child didn't crash as expected */
}
#endif