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
953 lines
30 KiB
C
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
|