Net-SNMP was crashing (signal 11) on every operation despite fork isolation.
The issue was that init_snmp() tries to load MIB files by default, which can
cause crashes if MIB files are missing, corrupted, or incompatible.
Since we only use numeric OIDs (e.g., "1.3.6.1.2.1.1.1.0"), we don't need
MIB name resolution at all. This change explicitly disables MIB loading in
the child process by:
1. Setting MIBS and MIBDIRS environment variables to empty
2. Calling netsnmp_set_mib_directory("") to disable MIB directory search
3. These must happen BEFORE calling init_snmp()
This should fix the persistent SIGSEGV crashes in SNMP operations.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
905 lines
28 KiB
C
905 lines
28 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;
|
|
|
|
switch (var->type) {
|
|
case ASN_OCTET_STR:
|
|
case ASN_OPAQUE:
|
|
case ASN_IPADDRESS:
|
|
if (var->val_len <= value_buf_len) {
|
|
memcpy(value_buf, var->val.string, var->val_len);
|
|
result = (int)var->val_len;
|
|
} 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 (sizeof(long) <= value_buf_len) {
|
|
*((long*)value_buf) = *var->val.integer;
|
|
result = sizeof(long);
|
|
}
|
|
break;
|
|
|
|
case ASN_COUNTER64:
|
|
if (sizeof(struct counter64) <= value_buf_len) {
|
|
memcpy(value_buf, var->val.counter64, sizeof(struct counter64));
|
|
result = sizeof(struct counter64);
|
|
}
|
|
break;
|
|
|
|
case ASN_OBJECT_ID:
|
|
// Convert OID to string representation
|
|
{
|
|
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;
|
|
}
|
|
|
|
// 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
|
|
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_len <= sizeof(res->value)) {
|
|
memcpy(res->value, var->val.string, var->val_len);
|
|
res->value_len = var->val_len;
|
|
}
|
|
break;
|
|
|
|
case ASN_OBJECT_ID:
|
|
// Convert OID to string representation
|
|
{
|
|
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 (sizeof(long) <= sizeof(res->value)) {
|
|
*((long*)res->value) = *var->val.integer;
|
|
res->value_len = sizeof(long);
|
|
}
|
|
break;
|
|
|
|
case ASN_COUNTER64:
|
|
if (sizeof(struct counter64) <= sizeof(res->value)) {
|
|
memcpy(res->value, var->val.counter64, sizeof(struct counter64));
|
|
res->value_len = sizeof(struct counter64);
|
|
}
|
|
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 */
|
|
setenv("MIBS", "", 1); /* Don't load any MIBs */
|
|
setenv("MIBDIRS", "", 1); /* Don't search for MIB directories */
|
|
netsnmp_set_mib_directory(""); /* Explicitly set empty MIB directory */
|
|
|
|
/* Initialize net-snmp fresh in child */
|
|
init_snmp("towerops-child");
|
|
netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID,
|
|
NETSNMP_DS_LIB_OID_OUTPUT_FORMAT,
|
|
NETSNMP_OID_OUTPUT_NUMERIC);
|
|
|
|
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 */
|
|
setenv("MIBS", "", 1); /* Don't load any MIBs */
|
|
setenv("MIBDIRS", "", 1); /* Don't search for MIB directories */
|
|
netsnmp_set_mib_directory(""); /* Explicitly set empty MIB directory */
|
|
|
|
/* Initialize net-snmp fresh in child */
|
|
init_snmp("towerops-child");
|
|
netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID,
|
|
NETSNMP_DS_LIB_OID_OUTPUT_FORMAT,
|
|
NETSNMP_OID_OUTPUT_NUMERIC);
|
|
|
|
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
|