Add MUSL static binary build system using Alpine Docker

- Create Dockerfile.alpine-musl for truly portable static binaries
- Update build_static.sh to use Docker with sudo fallback
- Fix source code portability issues for MUSL:
  * Add missing headers in config.c, dm_admin.c
  * Remove glibc-specific headers in nip009.c, subscriptions.c
- Update nostr_core_lib submodule with fortification fix
- Add comprehensive documentation in docs/musl_static_build.md

Binary characteristics:
- Size: 7.6MB (vs 12MB+ for glibc static)
- Dependencies: Zero (truly portable)
- Compatibility: Any Linux distribution
- Build time: ~2 minutes with Docker caching

Resolves fortification symbol issues (__snprintf_chk, __fprintf_chk)
that prevented MUSL static linking.
This commit is contained in:
Your Name
2025-10-11 10:17:20 -04:00
parent 6709e229b3
commit d449513861
8 changed files with 1007 additions and 184 deletions

View File

@@ -11,6 +11,7 @@
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>
#include <signal.h>
#include <libwebsockets.h>
// External database connection (from main.c)

View File

@@ -8,6 +8,7 @@
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cjson/cJSON.h>
#include <libwebsockets.h>

View File

@@ -11,7 +11,6 @@
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <printf.h>
// Forward declarations for logging functions
void log_warning(const char* message);

View File

@@ -5,7 +5,6 @@
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <printf.h>
#include <pthread.h>
#include <libwebsockets.h>
#include "subscriptions.h"
@@ -21,6 +20,13 @@ const char* get_config_value(const char* key);
// Forward declarations for NIP-40 expiration functions
int is_event_expired(cJSON* event, time_t current_time);
// Forward declarations for filter validation
int validate_filter_values(cJSON* filter_json, char* error_message, size_t error_size);
int validate_hex_string(const char* str, size_t expected_len, const char* field_name, char* error_message, size_t error_size);
int validate_timestamp_range(long since, long until, char* error_message, size_t error_size);
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
// Global database variable
extern sqlite3* g_db;
@@ -42,7 +48,14 @@ subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
if (!filter_json || !cJSON_IsObject(filter_json)) {
return NULL;
}
// Validate filter values before creating the filter
char error_message[512] = {0};
if (!validate_filter_values(filter_json, error_message, sizeof(error_message))) {
log_warning(error_message);
return NULL;
}
subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t));
if (!filter) {
return NULL;
@@ -111,28 +124,66 @@ void free_subscription_filter(subscription_filter_t* filter) {
free(filter);
}
// Validate subscription ID format and length
static int validate_subscription_id(const char* sub_id) {
if (!sub_id) {
return 0; // NULL pointer
}
size_t len = strlen(sub_id);
if (len == 0 || len >= SUBSCRIPTION_ID_MAX_LENGTH) {
return 0; // Empty or too long
}
// Check for valid characters (alphanumeric, underscore, hyphen)
for (size_t i = 0; i < len; i++) {
char c = sub_id[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
return 0; // Invalid character
}
}
return 1; // Valid
}
// Create a new subscription
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
if (!sub_id || !wsi || !filters_array) {
log_error("create_subscription: NULL parameter(s)");
return NULL;
}
// Validate subscription ID
if (!validate_subscription_id(sub_id)) {
log_error("create_subscription: invalid subscription ID format or length");
return NULL;
}
subscription_t* sub = calloc(1, sizeof(subscription_t));
if (!sub) {
log_error("create_subscription: failed to allocate subscription");
return NULL;
}
// Copy subscription ID (truncate if too long)
strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
// Copy subscription ID safely (already validated)
size_t id_len = strlen(sub_id);
memcpy(sub->id, sub_id, id_len);
sub->id[id_len] = '\0';
// Set WebSocket connection
sub->wsi = wsi;
// Set client IP
// Set client IP safely
if (client_ip) {
strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
size_t ip_len = strlen(client_ip);
if (ip_len >= CLIENT_IP_MAX_LENGTH) {
ip_len = CLIENT_IP_MAX_LENGTH - 1;
}
memcpy(sub->client_ip, client_ip, ip_len);
sub->client_ip[ip_len] = '\0';
} else {
sub->client_ip[0] = '\0'; // Ensure null termination
}
// Set timestamps and state
@@ -215,42 +266,61 @@ int add_subscription_to_manager(subscription_t* sub) {
// Remove subscription from global manager (thread-safe)
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
if (!sub_id) return -1;
if (!sub_id) {
log_error("remove_subscription_from_manager: NULL subscription ID");
return -1;
}
// Validate subscription ID format
if (!validate_subscription_id(sub_id)) {
log_error("remove_subscription_from_manager: invalid subscription ID format");
return -1;
}
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
subscription_t** current = &g_subscription_manager.active_subscriptions;
while (*current) {
subscription_t* sub = *current;
// Match by ID and WebSocket connection
if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) {
// Remove from list
*current = sub->next;
g_subscription_manager.total_subscriptions--;
// Copy data needed for logging before unlocking
char client_ip_copy[CLIENT_IP_MAX_LENGTH];
int events_sent_copy = sub->events_sent;
char sub_id_copy[SUBSCRIPTION_ID_MAX_LENGTH];
memcpy(client_ip_copy, sub->client_ip, CLIENT_IP_MAX_LENGTH);
memcpy(sub_id_copy, sub->id, SUBSCRIPTION_ID_MAX_LENGTH);
client_ip_copy[CLIENT_IP_MAX_LENGTH - 1] = '\0';
sub_id_copy[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
// Log subscription closure to database
log_subscription_closed(sub_id, sub->client_ip, "closed");
// Log subscription closure to database (now safe)
log_subscription_closed(sub_id_copy, client_ip_copy, "closed");
// Update events sent counter before freeing
update_subscription_events_sent(sub_id, sub->events_sent);
update_subscription_events_sent(sub_id_copy, events_sent_copy);
free_subscription(sub);
return 0;
}
current = &(sub->next);
}
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
log_warning(debug_msg);
return -1;
}
@@ -493,13 +563,28 @@ int broadcast_event_to_subscriptions(cJSON* event) {
temp_sub_t* temp = malloc(sizeof(temp_sub_t));
if (temp) {
temp->wsi = sub->wsi;
strncpy(temp->id, sub->id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
temp->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
strncpy(temp->client_ip, sub->client_ip, CLIENT_IP_MAX_LENGTH - 1);
temp->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
// Safely copy subscription ID
size_t id_len = strlen(sub->id);
if (id_len >= SUBSCRIPTION_ID_MAX_LENGTH) {
id_len = SUBSCRIPTION_ID_MAX_LENGTH - 1;
}
memcpy(temp->id, sub->id, id_len);
temp->id[id_len] = '\0';
// Safely copy client IP
size_t ip_len = strlen(sub->client_ip);
if (ip_len >= CLIENT_IP_MAX_LENGTH) {
ip_len = CLIENT_IP_MAX_LENGTH - 1;
}
memcpy(temp->client_ip, sub->client_ip, ip_len);
temp->client_ip[ip_len] = '\0';
temp->next = matching_subs;
matching_subs = temp;
matching_count++;
} else {
log_error("broadcast_event_to_subscriptions: failed to allocate temp subscription");
}
}
sub = sub->next;
@@ -884,3 +969,330 @@ int get_active_connections_for_ip(const char* client_ip) {
pthread_mutex_unlock(&g_subscription_manager.ip_tracking_lock);
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// FILTER VALIDATION FUNCTIONS
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
/**
* Validate hex string format and length
*/
int validate_hex_string(const char* str, size_t expected_len, const char* field_name, char* error_message, size_t error_size) {
if (!str) {
snprintf(error_message, error_size, "%s: null value", field_name);
return 0;
}
size_t len = strlen(str);
if (len != expected_len) {
snprintf(error_message, error_size, "%s: invalid length %zu, expected %zu", field_name, len, expected_len);
return 0;
}
// Check for valid hex characters
for (size_t i = 0; i < len; i++) {
char c = str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
snprintf(error_message, error_size, "%s: invalid hex character '%c' at position %zu", field_name, c, i);
return 0;
}
}
return 1;
}
/**
* Validate timestamp range (since/until)
*/
int validate_timestamp_range(long since, long until, char* error_message, size_t error_size) {
// Allow zero values (not set)
if (since == 0 && until == 0) {
return 1;
}
// Check for reasonable timestamp bounds (1970-01-01 to 2100-01-01)
if (since != 0 && (since < MIN_TIMESTAMP || since > MAX_TIMESTAMP)) {
snprintf(error_message, error_size, "since: timestamp %ld out of valid range", since);
return 0;
}
if (until != 0 && (until < MIN_TIMESTAMP || until > MAX_TIMESTAMP)) {
snprintf(error_message, error_size, "until: timestamp %ld out of valid range", until);
return 0;
}
// Check that since is before until if both are set
if (since > 0 && until > 0 && since >= until) {
snprintf(error_message, error_size, "since (%ld) must be before until (%ld)", since, until);
return 0;
}
return 1;
}
/**
* Validate numeric limits
*/
int validate_numeric_limits(int limit, char* error_message, size_t error_size) {
// Allow zero (no limit)
if (limit == 0) {
return 1;
}
// Check for reasonable limits (1-10000)
if (limit < MIN_LIMIT || limit > MAX_LIMIT) {
snprintf(error_message, error_size, "limit: value %d out of valid range [%d, %d]", limit, MIN_LIMIT, MAX_LIMIT);
return 0;
}
return 1;
}
/**
* Validate search term for SQL injection and length
*/
int validate_search_term(const char* search_term, char* error_message, size_t error_size) {
if (!search_term) {
return 1; // NULL search terms are allowed
}
size_t len = strlen(search_term);
// Check maximum length
if (len > MAX_SEARCH_TERM_LENGTH) {
snprintf(error_message, error_size, "search: term too long (%zu characters, max %d)", len, (int)MAX_SEARCH_TERM_LENGTH);
return 0;
}
// Check for potentially dangerous characters that could cause SQL issues
// Allow alphanumeric, spaces, and common punctuation
for (size_t i = 0; i < len; i++) {
char c = search_term[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == ' ' || c == '-' || c == '_' ||
c == '.' || c == ',' || c == '!' || c == '?' || c == ':' ||
c == ';' || c == '"' || c == '\'' || c == '(' || c == ')' ||
c == '[' || c == ']' || c == '{' || c == '}' || c == '@' ||
c == '#' || c == '$' || c == '%' || c == '^' || c == '&' ||
c == '*' || c == '+' || c == '=' || c == '|' || c == '\\' ||
c == '/' || c == '<' || c == '>' || c == '~' || c == '`')) {
// Reject control characters and other potentially problematic chars
if (c < 32 || c == 127) {
snprintf(error_message, error_size, "search: invalid character (ASCII %d) at position %zu", (int)c, i);
return 0;
}
}
}
return 1;
}
/**
* Validate all filter values in a filter object
*/
int validate_filter_values(cJSON* filter_json, char* error_message, size_t error_size) {
if (!filter_json || !cJSON_IsObject(filter_json)) {
snprintf(error_message, error_size, "filter must be a JSON object");
return 0;
}
// Validate kinds array
cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds");
if (kinds) {
if (!cJSON_IsArray(kinds)) {
snprintf(error_message, error_size, "kinds must be an array");
return 0;
}
int kinds_count = cJSON_GetArraySize(kinds);
if (kinds_count > MAX_KINDS_PER_FILTER) {
snprintf(error_message, error_size, "kinds array too large (%d items, max %d)", kinds_count, MAX_KINDS_PER_FILTER);
return 0;
}
for (int i = 0; i < kinds_count; i++) {
cJSON* kind_item = cJSON_GetArrayItem(kinds, i);
if (!cJSON_IsNumber(kind_item)) {
snprintf(error_message, error_size, "kinds[%d] must be a number", i);
return 0;
}
int kind_val = (int)cJSON_GetNumberValue(kind_item);
if (kind_val < 0 || kind_val > 65535) { // Reasonable range for event kinds
snprintf(error_message, error_size, "kinds[%d]: invalid event kind %d", i, kind_val);
return 0;
}
}
}
// Validate authors array
cJSON* authors = cJSON_GetObjectItem(filter_json, "authors");
if (authors) {
if (!cJSON_IsArray(authors)) {
snprintf(error_message, error_size, "authors must be an array");
return 0;
}
int authors_count = cJSON_GetArraySize(authors);
if (authors_count > MAX_AUTHORS_PER_FILTER) {
snprintf(error_message, error_size, "authors array too large (%d items, max %d)", authors_count, MAX_AUTHORS_PER_FILTER);
return 0;
}
for (int i = 0; i < authors_count; i++) {
cJSON* author_item = cJSON_GetArrayItem(authors, i);
if (!cJSON_IsString(author_item)) {
snprintf(error_message, error_size, "authors[%d] must be a string", i);
return 0;
}
const char* author_str = cJSON_GetStringValue(author_item);
// Allow partial pubkeys (prefix matching), so validate hex but allow shorter lengths
size_t author_len = strlen(author_str);
if (author_len == 0 || author_len > 64) {
snprintf(error_message, error_size, "authors[%d]: invalid length %zu", i, author_len);
return 0;
}
// Validate hex characters (allow partial)
for (size_t j = 0; j < author_len; j++) {
char c = author_str[j];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
snprintf(error_message, error_size, "authors[%d]: invalid hex character '%c'", i, c);
return 0;
}
}
}
}
// Validate ids array
cJSON* ids = cJSON_GetObjectItem(filter_json, "ids");
if (ids) {
if (!cJSON_IsArray(ids)) {
snprintf(error_message, error_size, "ids must be an array");
return 0;
}
int ids_count = cJSON_GetArraySize(ids);
if (ids_count > MAX_IDS_PER_FILTER) {
snprintf(error_message, error_size, "ids array too large (%d items, max %d)", ids_count, MAX_IDS_PER_FILTER);
return 0;
}
for (int i = 0; i < ids_count; i++) {
cJSON* id_item = cJSON_GetArrayItem(ids, i);
if (!cJSON_IsString(id_item)) {
snprintf(error_message, error_size, "ids[%d] must be a string", i);
return 0;
}
const char* id_str = cJSON_GetStringValue(id_item);
// Allow partial IDs (prefix matching)
size_t id_len = strlen(id_str);
if (id_len == 0 || id_len > 64) {
snprintf(error_message, error_size, "ids[%d]: invalid length %zu", i, id_len);
return 0;
}
// Validate hex characters
for (size_t j = 0; j < id_len; j++) {
char c = id_str[j];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
snprintf(error_message, error_size, "ids[%d]: invalid hex character '%c'", i, c);
return 0;
}
}
}
}
// Validate since/until timestamps
long since_val = 0, until_val = 0;
cJSON* since = cJSON_GetObjectItem(filter_json, "since");
if (since) {
if (!cJSON_IsNumber(since)) {
snprintf(error_message, error_size, "since must be a number");
return 0;
}
since_val = (long)cJSON_GetNumberValue(since);
}
cJSON* until = cJSON_GetObjectItem(filter_json, "until");
if (until) {
if (!cJSON_IsNumber(until)) {
snprintf(error_message, error_size, "until must be a number");
return 0;
}
until_val = (long)cJSON_GetNumberValue(until);
}
if (!validate_timestamp_range(since_val, until_val, error_message, error_size)) {
return 0;
}
// Validate limit
cJSON* limit = cJSON_GetObjectItem(filter_json, "limit");
if (limit) {
if (!cJSON_IsNumber(limit)) {
snprintf(error_message, error_size, "limit must be a number");
return 0;
}
int limit_val = (int)cJSON_GetNumberValue(limit);
if (!validate_numeric_limits(limit_val, error_message, error_size)) {
return 0;
}
}
// Validate search term
cJSON* search = cJSON_GetObjectItem(filter_json, "search");
if (search) {
if (!cJSON_IsString(search)) {
snprintf(error_message, error_size, "search must be a string");
return 0;
}
const char* search_term = cJSON_GetStringValue(search);
if (!validate_search_term(search_term, error_message, error_size)) {
return 0;
}
}
// Validate tag filters (#e, #p, #t, etc.)
cJSON* item = NULL;
cJSON_ArrayForEach(item, filter_json) {
const char* key = item->string;
if (key && strlen(key) >= 2 && key[0] == '#') {
if (!cJSON_IsArray(item)) {
snprintf(error_message, error_size, "%s must be an array", key);
return 0;
}
int tag_count = cJSON_GetArraySize(item);
if (tag_count > MAX_TAG_VALUES_PER_FILTER) {
snprintf(error_message, error_size, "%s array too large (%d items, max %d)", key, tag_count, MAX_TAG_VALUES_PER_FILTER);
return 0;
}
for (int i = 0; i < tag_count; i++) {
cJSON* tag_value = cJSON_GetArrayItem(item, i);
if (!cJSON_IsString(tag_value)) {
snprintf(error_message, error_size, "%s[%d] must be a string", key, i);
return 0;
}
const char* tag_str = cJSON_GetStringValue(tag_value);
size_t tag_len = strlen(tag_str);
if (tag_len > MAX_TAG_VALUE_LENGTH) {
snprintf(error_message, error_size, "%s[%d]: tag value too long (%zu characters, max %d)", key, i, tag_len, MAX_TAG_VALUE_LENGTH);
return 0;
}
}
}
}
return 1;
}