Skip to content

Feat/protobuf log format - #196

Open
doomedraven wants to merge 16 commits into
kevoreilly:capemonfrom
doomedraven:feat/protobuf-log-format
Open

Feat/protobuf log format#196
doomedraven wants to merge 16 commits into
kevoreilly:capemonfrom
doomedraven:feat/protobuf-log-format

Conversation

@doomedraven

Copy link
Copy Markdown
Contributor

No description provided.

doomedraven and others added 16 commits August 19, 2026 12:36
…(SBO-Decoupling)

Implements completely concurrent and thread-local log serialization inside loq. Makes g_bson and g_istr thread-local variables using __declspec(thread), allowing multiple monitored threads to format their API arguments lock-free. Holds the global g_mutex strictly during the actual BSON buffer flush/cache operations, dropping lock-hold times from milliseconds to microseconds.
…2 Fix)

Surgically fixes the fatal crash bug caused by illegal static TLS usage (__declspec(thread)) inside the dynamically injected capemon.dll:
1. Replaces the unsupported static TLS variables g_bson and g_istr with safe, dynamic Windows Thread Local Storage (TLS) API (TlsAlloc, TlsGetValue, TlsSetValue, TlsFree).
2. Maps g_bson and g_istr through preprocessor macros to dynamic, auto-allocated thread contexts (thread_log_context_t) on-the-fly, retaining 100% compatibility with all 50+ logging helper functions.
3. Automatically frees thread-local log contexts during DLL_THREAD_DETACH inside DllMain to guarantee absolute zero memory leaks.
…zation

Addresses three critical defects in the concurrent logging implementation:

1. NULL Pointer Dereference Protection:
   - Added null check when calloc() fails in GetThreadLogContext()
   - Added null-safe accessor macros for g_bson and g_istr
   - Added early TLS validation in loq() before any logging operations
   - Prevents crashes when TLS allocation fails

2. Race Condition Fix in logtbl_explained:
   - Fixed broken double-checked locking with volatile cast
   - Added proper memory ordering: *(volatile char*)&logtbl_explained[index]
   - Replaced unsafe goto skip_explain with early return + cleanup
   - Ensures thread-safe initialization of log table explanations

3. Performance Optimization with __declspec(thread):
   - Added g_tls_ctx_cache using __declspec(thread) as described in PR
   - GetThreadLogContext() now returns cached value after first lookup
   - Eliminates repeated expensive TlsGetValue() calls on hot path
   - Cache cleared properly in TlsThreadCleanup()

The hybrid TLS approach (TLS API + __declspec(thread) cache) provides:
- Cross-DLL thread tracking compatibility
- Fast repeated access within same thread
- Proper cleanup on thread detach

All changes maintain 100% backward compatibility.
Test coverage:
- Concurrent logging from 16 threads (80,000 log operations)
- Rapid thread creation/destruction (TLS stress test)
- logtbl_explained race condition test (32 threads, same index)

Verifies all three critical fixes:
1. NULL pointer protection (TLS allocation failures)
2. Race condition fix (volatile + double-checked locking)
3. Performance optimization (__declspec(thread) cache)

Run with: cd tests && make test-tls-logging.exe && ./test-tls-logging.exe
…obuf)

Introduces a highly flexible, pluggable logging interface (g_active_serializer Strategy Pattern) supporting both BSON and Protocol Buffers dynamically:
1. Retains BSON as the 100% backward-compatible default serializer (preserving full compatibility for custom agents and result servers).
2. Adds high-performance, robust, and safe Protocol Buffers logging (via nanopb) which can be enabled dynamically at runtime using the config option "log-format = 1".
3. Fully resolves the critical UAF memory lifecycles bug on wide strings inside protobuf_wrapper.c by implementing a fast, zero-allocation, thread-local string and binary scratch-pad bump allocator.
4. Increases the nanopb serialization buffer size from 4KB to 64KB (allocated on static thread-local context structures) to safely prevent large payloads and decrypted config drops.
Test coverage:
- BSON serialization (default mode)
- Protobuf serialization (opt-in mode)
- Runtime serializer switching
- Thread-local serializer isolation (16 threads)
- Concurrent mixed serializers (8 threads, BSON + Protobuf)
- NULL safety in serializer access

Verifies:
1. Strategy pattern implementation
2. Thread-safe serializer switching
3. Independent per-thread serializer contexts
4. Graceful fallback on NULL
5. No interference between BSON and Protobuf modes

Run with: cd tests && make test-pluggable-serialization.exe && ./test-pluggable-serialization.exe
…efault_serializer and including log_serializer.h
The pluggable-serializer refactor introduced several regressions on the
default BSON path and left the protobuf backend unable to represent the
call model. This restores BSON wire compatibility, fixes the string
length handling, tightens the locking, and gates protobuf as explicitly
experimental.

log_serializer.h / log.c / protobuf_wrapper.*:
- append_string/append_wstring regain an explicit `length` parameter.
  Callers pass counted, non-NUL-terminated buffers (%S, %U, %o, registry
  values); the previous signatures forced strlen()/lstrlenW() on the raw
  input, over-reading process memory (crash or disclosure into the log).
- BSON string append restored to the historical encoding: every unit
  through utf8_do_encode() then stored as BSON_BIN_BINARY, with the
  stack-buffer fast path and the ""-on-OOM/error fallback. The interim
  code emitted a raw bson_append_string() that truncated at embedded NULs
  and could be rejected by the result-server parser as invalid UTF-8.
- serializer_append_ptr() helper replaces the open-coded C/R/P/return
  handling: int32 on 32-bit, int64 on 64-bit, one width for every pointer
  field (the interim code emitted C as int32 but R/P as int64 on x86).
- special_api_triggered / last_api_logged / delete_last_log are consumed
  in a short critical section BEFORE serialization again. Serialization
  now runs unlocked into thread-local buffers, so consuming this shared
  state at the tail let a concurrent loq() see stale values or free
  lastlog.buf out from under the API set_special_api() targeted.
- The per-index BSON "explain" frame and the residual
  bson_append_binary(g_bson,...) calls in the %r/%R/buffer_log paths now
  route through the active serializer, so protobuf mode no longer
  interleaves BSON frames into its output stream.
- protobuf_context_t (~100 KB: encode buffer + string scratch) is now a
  lazily-allocated pointer in thread_log_context_t, allocated only on a
  thread's first protobuf log. Default BSON mode allocates nothing extra
  (previously every logging thread paid ~100 KB of zeroed memory).
- g_bson / g_istr / g_active_serializer are single-lookup __inline
  accessors (were two TLS lookups per macro expansion); loq() caches the
  serializer in a local for the hot path.
- protobuf T no longer overwrites call->t (thread id has no schema field
  and is dropped explicitly); scratch-copy honours the length; both
  serializer tables use designated initializers.
- log_init() emits a CRITICAL warning when log-format=1 is selected:
  protobuf output is experimental and lossy and has no host-side parser.

Builds clean on Release|Win32 and Release|x64 (MSVC v143), no warnings
in log.c / protobuf_wrapper.c.

Next:
- Protobuf as a real BSON replacement is a separate effort: redesign
  schema.proto to carry the full call model (heterogeneous indexed args,
  nested %a arrays, caller address, thread id), regenerate schema.pb.*
  with the nanopb generator (not available in this env), grow/size the
  protobuf scratch arena to large_buffer_log_max, give the netlog
  transport its own protocol header, and add a matching parser on the
  CAPE result-server side. Only then drop the experimental banner.
- Benchmark protobuf vs BSON encode cost + wire size before switching any
  default; this BSON writer is a trivial TLV appender and nanopb's
  callback-per-field model may not be faster.
- test-pluggable-serialization.c is still a smoke test (the format is
  process-global, latched at log_init; it cannot switch at runtime).
  A real test needs a full monitor build to assert on emitted bytes and
  to exercise the counted-string / no-over-read paths.
Replace the ad-hoc StrMessage/RegularCall schema with the schema CAPEv2's
result server already expects (lib/cuckoo/common/netlog.py ProtobufParser,
data/capemon_pb.proto): HookEvent{ oneof payload { InfoMessage; CallMessage;
ProcessMessage; DebugMessage } }. The only intentional delta is
CallMessage.arguments/.aux as `repeated bytes` (capemon args carry raw
non-UTF-8 buffers).

- schema.proto: verbatim copy of CAPEv2's proto (package capemon) with
  repeated bytes for arguments/aux.
- schema.options + scripts/gen-schema.sh: nanopb field config and a
  reproducible regen step (pip nanopb 0.4.9.1 generator + grpcio-tools,
  PB_PROTO_HEADER_VERSION 40 matches the vendored nanopb/ runtime).
  InfoMessage/ArgumentInfo/ProcessMessage/DebugMessage use bounded static
  storage; only CallMessage.arguments/.aux stay FT_CALLBACK.
- schema.pb.{c,h}: regenerated.
- protobuf_wrapper.{c,h}: rewritten for the new schema. Per-thread context
  drops from ~100 KB to ~4 KB. CallMessage.arguments is streamed from a
  growable arena of (offset,len) slots via one encode callback; %a/%A
  arrays are flattened into a single NUL-separated slot; scalar int args
  are rendered as decimal text. Adds standalone protobuf_encode_info() /
  protobuf_encode_debug() for the frames loq() does not build field by
  field.
- log.c: the per-index explain frame is now emitted in both formats - BSON
  "info" document or protobuf InfoMessage (arg names/types collected during
  the same walk). announce_netlog() sends "PROTO <pid>" instead of "BSON
  <pid>" in protobuf mode; debug_message() emits DebugMessage.
  TlsThreadCleanup frees the protobuf arena/out buffers.

Builds clean Release|Win32 and Release|x64 (MSVC v143).

Still TODO before dropping the "needs CAPEv2 support" notice:
- CAPEv2 side: fix capemon_pb2 import/filename mismatch; arguments/aux ->
  repeated bytes + regen capemon_pb2.py; wire ProtobufParser into
  behavior.py ParseProcessLog for .protobuf logs; special-case
  __process__/__thread__/__environ__ like BsonParser does; decide how
  typed (h/p) args are rendered from bytes + InfoMessage.type.
- Benchmark encode cost + wire size vs BSON.
- Real round-trip test (encode in a monitor build, decode with capemon_pb2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fq8SRqMeNFnRZ7rMy21jd2
The protobuf CallMessage carries every argument as raw bytes, so the parser
needs the InfoMessage to say which ones are integers. Extend the explain
walk (protobuf path only - BSON elements stay natively typed) to tag:
  i/I/l/L -> "i"  (bare integer; parser does int(text))
  a/A     -> "a"  (flattened NUL-separated array; parser splits)
p/P/h/H (-> "h"/"p") and x/X (-> "p") were already tagged for hex display.
This lets ProtobufParser rebuild argdict with the same Python types
BsonParser produces (needed for __process__/__thread__ reconstruction).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant