Each SNMP GET/WALK now runs in a forked child process. If libnetsnmp triggers a SIGSEGV or other fatal signal, only the child dies - the parent agent logs the crash and continues operating for all other devices. Key changes: - C helper: snmp_get_isolated() and snmp_walk_isolated() using fork+pipe pattern with 60s alarm watchdog and mutex-serialized forks - Rust: IsolationMode enum (Fork/Direct) controlled by TOWEROPS_SNMP_ISOLATION env var, defaults to Fork - New CrashRecovered error variant with signal info and logging - Device poller logs crash recovery events at error level - Startup logs active isolation mode Set TOWEROPS_SNMP_ISOLATION=direct to disable isolation for debugging.
895 lines
28 KiB
C
895 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 */
|
|
|
|
/* 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 */
|
|
|
|
/* 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
|