You are viewing a plain text version of this content. The canonical link for it is here.
Posted to c-commits@axis.apache.org by ro...@apache.org on 2026/09/04 20:37:05 UTC
(axis-axis2-c-core) 02/02: Warn that the two _invoke_json forms are one symbol
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 9ff9fb68edd41f039e23bb3a3b8eb2a770560aa8
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 09:15:39 2026 -1000
Warn that the two _invoke_json forms are one symbol
The doc described both signatures without saying they share a name, so a
binary cannot hold both and an Android link has to exclude the object carrying
the four-argument one. That object usually also carries axis2_get_instance,
which the class-loader path needs, so excluding it leaves the static registry
as the only route in -- and a service missing from the registry logs at INFO
and falls through rather than failing.
Adds that, the logcat lines that tell the cases apart, and how to read the
registry out of a built binary: --export-dynamic keeps .dynsym through
stripping, so nm -D plus a short disassembly of android_static_service_lookup
shows which services are really registered. Registry entries have been lost to
a clean rebuild before; this is how to notice.
Co-Authored-By: Claude <no...@anthropic.com>
---
docs/HTTP2_ANDROID.md | 1550 ++++++++++++++++++++++++++-----------------------
1 file changed, 817 insertions(+), 733 deletions(-)
diff --git a/docs/HTTP2_ANDROID.md b/docs/HTTP2_ANDROID.md
index 89d16eec5..973a8f85c 100644
--- a/docs/HTTP2_ANDROID.md
+++ b/docs/HTTP2_ANDROID.md
@@ -1,733 +1,817 @@
-# Axis2/C Android Support
-
-This document describes how Axis2/C supports Android deployment with HTTP/2 and JSON-RPC.
-
-## Quick Reference for Claude
-
-When working with Axis2/C Android support, remember:
-
-1. **All builds use `-std=gnu99`** - Required for json-c header compatibility
-2. **All builds use `-fsigned-char`** - `axis2_char_t` is plain `char`, which is
- signed on x86 but unsigned on ARM. See "Char signedness" below; an app that
- compiles its own service sources must pass this flag too.
-3. **Android uses static linking** - No dlopen(), services use weak symbol registry
-4. **Service code lives in applications** - Axis2/C core only has weak symbol stubs
-5. **`--whole-archive` is required** - For weak/strong symbol resolution during linking
-6. **IPC uses fork()/execvp()** - NEVER use system() for security (command injection)
-7. **Java validates all input** - SecurityValidator class provides defense-in-depth
-
-## Build System
-
-### C Standard: Always gnu99
-
-All Axis2/C builds use `-std=gnu99` (GNU C99 mode). This is required because:
-
-1. **json-c headers** use C99 `inline` keyword
-2. **Android NDK headers** require GNU extensions
-3. **POSIX functions** are hidden by strict ANSI mode
-
-**Implementation in the root configure.ac:**
-
-```m4
-if test "$GCC" = "yes"; then
- dnl Use gnu99 for json-c compatibility
- CFLAGS="$CFLAGS -std=gnu99 -Wall -Wno-implicit-function-declaration -g -D_GNU_SOURCE"
- CFLAGS="$CFLAGS -Wformat-security"
- CFLAGS="$CFLAGS -Wsign-compare"
- CFLAGS="$CFLAGS -fsigned-char"
- CXXFLAGS="$CXXFLAGS -fsigned-char"
-fi
-```
-
-Seven configure.ac files carry their own `-std=gnu99` block:
-
-- `configure.ac` (root), `util/`, `axiom/`, `neethi/`, `guththila/`,
- `samples/`, `tools/md5/`
-
-`samples/` builds with `-Werror`, so warning flags must not be added to it
-casually — a warning in a sample would fail the build rather than inform
-anyone. It gets `-fsigned-char` (a codegen flag, no warning risk) but not
-`-Wsign-compare`.
-
-**Each sub-package must set the hardening flags itself.** `util`, `axiom`,
-`neethi` and `guththila` are separate packages configured through
-`AC_CONFIG_SUBDIRS`, and they do **not** inherit `CFLAGS` additions the root
-configure makes to its own shell variable. A flag added only to the root
-configure.ac reaches `src/` and nothing else.
-
-This is easy to get wrong and hard to see, because a build with the flag missing
-succeeds and looks identical. Check the *generated Makefiles* rather than the
-build output:
-
-```bash
-for m in Makefile util/src/Makefile axiom/src/om/Makefile \
- neethi/src/Makefile guththila/src/Makefile; do
- printf "%-28s " "$m"
- grep -m1 '^CFLAGS = ' "$m" | grep -q 'fsigned-char' && echo OK || echo MISSING
-done
-```
-
-Counting flagged compile lines in the make log is *not* a valid check: most
-compiles name their source relative to the current directory, so any
-per-directory grep silently samples a handful of lines and reports a clean
-result for a package that has none.
-
-When adding a compiler flag, add it to all five configure.ac files together.
-
-### Char signedness: why `-fsigned-char` is not optional
-
-`axis2_char_t` is `typedef char` — plain `char`, whose signedness is
-implementation-defined. It is **signed on x86-64 and unsigned on aarch64**.
-Axis2/C was written and is tested on x86, and it assumes the signed reading in
-places: there are `ctype` calls that pass an `axis2_char_t` straight to
-`isxdigit()` and friends without the `(unsigned char)` cast those macros
-require (for example `util/src/utils.c:543`). A byte with the high bit set
-reaches them as a negative int on one architecture and as 128-255 on the other.
-
-Without the flag the same source is a different program on the device than on
-the build machine, and any testing done on x86 stops transferring. With it, an
-x86 test run says something about arm64.
-
-Remove it only together with an audit of every `char` that crosses a `ctype`
-call or a comparison against a negative value.
-
-**Applications compiling their own service sources must pass it too.** The
-libraries are built with `-fsigned-char` via configure, but an app's own build
-script compiles its service `.c` files outside that, so without the flag the
-same `char` behaves differently on either side of the call:
-
-```bash
-$CC -fPIC -fsigned-char -D__ANDROID__ -c ... my_service.c -o my_service.o
-```
-
-### Android Detection
-
-Android builds are detected by the `--host` parameter:
-
-```m4
-AC_MSG_CHECKING([for Android target])
-case "$host" in
- *-linux-android*|*-android*)
- android_build=yes
- AC_DEFINE([AXIS2_ANDROID_BUILD], [1], [Building for Android])
- ;;
- *)
- android_build=no
- ;;
-esac
-```
-
-When `android_build=yes`:
-- `AXIS2_ANDROID_BUILD` is defined
-- `__ANDROID__` is typically already defined by the NDK compiler
-- Static linking is expected
-
-### Android NDK Cross-Compilation
-
-**Environment setup:**
-
-```bash
-export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/28.0.12916984
-export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64
-export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang
-export AR=$TOOLCHAIN/bin/llvm-ar
-export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
-```
-
-**Configure command:**
-
-```bash
-./configure \
- --host=aarch64-linux-android \
- --prefix=$DEPS \
- --enable-static \
- --disable-shared \
- --enable-http2 \
- --enable-json \
- --with-apr=$DEPS \
- --with-openssl=$DEPS
-```
-
-**Convenience script:** Use `build_android.sh` in the repository root for a complete build:
-
-```bash
-./build_android.sh
-```
-
-This script sets up the toolchain, configures with all required flags, and builds. Edit the script to adjust paths for your NDK installation and dependency locations.
-
-Note it does not pass `--with-apache2`, so it does not produce `libmod_axis2.a`.
-A build intended for an app that embeds httpd needs that argument.
-
-### Cross-build hazards
-
-Three things that fail quietly. All three were observed, not theorised.
-
-**The dependency directory goes stale without saying so.** Apps link static
-archives from a prefix such as `$DEPS/lib`. Nothing there records which source
-revision produced them, and nothing warns when the tree is newer. A prefix
-populated months ago will link cleanly and produce a working APK that contains
-none of the fixes made since. After any refresh, **compare timestamps across
-every library**, not a sample — a partial refresh leaves a mixed-date directory
-that looks fine at a glance:
-
-```bash
-ls -la $DEPS/lib/libaxis2*.a $DEPS/lib/libaxutil.a | awk '{print $6, $7, $9}'
-```
-
-**Objects do not record the flags they were built with.** Cross-configuring a
-tree that already holds a native build does not force a rebuild of everything.
-Always `make distclean` before switching host, and confirm afterwards:
-
-```bash
-find . -name '*.o' | wc -l # expect 0 before the cross build starts
-```
-
-**An install step that globs for archives will copy host-built ones.** A line
-like `find . -name "*.a" -path "*/.libs/*" -exec cp {} $DEPS/lib/ \;` cannot
-tell an arm64 archive from an x86-64 one, and `samples/` in particular survives
-`distclean` in some configurations. Filter by architecture rather than by path:
-
-```bash
-for f in $(find . -name '*.a' -path '*/.libs/*'); do
- ar p "$f" | file - | grep -q "ARM aarch64" && cp "$f" "$DEPS/lib/"
-done
-```
-
-### 16 KB page alignment (Android 15+)
-
-Devices with 16 KB pages will not load a shared library aligned for 4 KB. NDK
-r28 emits 16 KB-aligned `LOAD` segments by default; older NDKs need
-`-Wl,-z,max-page-size=16384`. Verify rather than assume:
-
-```bash
-readelf -lW libyour_httpd.so | awk '/LOAD/{print $NF}' | sort -u
-# want 0x4000 (16384); 0x1000 (4096) will fail to load on such a device
-```
-
-## Static Service Registry (Android)
-
-### The Problem
-
-Traditional Axis2/C loads services dynamically:
-
-```c
-void *lib = dlopen("libmyservice.so", RTLD_NOW);
-func = dlsym(lib, "service_invoke_json");
-```
-
-Android with static linking has no `.so` files to load.
-
-### Solution: Weak Symbol Architecture
-
-Axis2/C uses weak symbols so applications can provide service implementations at link time.
-
-**In `src/core/receivers/axis2_json_rpc_msg_recv.c`:**
-
-```c
-#ifdef __ANDROID__
-typedef json_object* (*json_service_invoke_func_t)(
- const axutil_env_t *env, json_object *json_request);
-
-/* Weak symbol - returns NULL if no implementation linked */
-__attribute__((weak))
-json_object* camera_control_service_invoke_json(
- const axutil_env_t *env, json_object *json_request)
-{
- (void)env;
- (void)json_request;
- return NULL;
-}
-
-/* Lookup function - returns function pointer for known services */
-json_service_invoke_func_t
-android_static_service_lookup(const char *service_name)
-{
- if (!service_name) return NULL;
-
- if (strcmp(service_name, "CameraControlService") == 0) {
- return camera_control_service_invoke_json;
- }
- if (strcmp(service_name, "AudioSearchService") == 0) {
- return audio_search_service_invoke_json;
- }
- /* Add more services here */
-
- return NULL;
-}
-#endif
-```
-
-### How It Works
-
-```
-[Axis2/C Framework] [Application]
- | |
- weak symbol: strong symbol:
- camera_control_service_ camera_control_service_
- invoke_json() -> NULL invoke_json() -> response
- | |
- +----------- Link Time ---------+
- |
- Strong symbol wins
-```
-
-1. **Framework compiles** with weak symbol stubs (return NULL)
-2. **Application provides** strong symbol implementations
-3. **At link time**, strong symbols override weak symbols
-4. **Result**: Application's service code handles requests
-
-### Application Service Implementation
-
-Applications provide two files:
-
-**Adapter (bridges Axis2/C to app):**
-
-```c
-// axis2_static_service_adapter.c
-#include <axutil_env.h>
-#include <json-c/json.h>
-
-extern int camera_control_service_impl(const char* req, char* resp, size_t size);
-
-/* Strong symbol overrides weak in Axis2/C */
-json_object* camera_control_service_invoke_json(
- const axutil_env_t *env, json_object *json_request)
-{
- const char *req_str = json_object_to_json_string(json_request);
- char response[65536] = {0};
-
- camera_control_service_impl(req_str, response, sizeof(response));
-
- return json_tokener_parse(response);
-}
-```
-
-**Implementation (app-specific logic):**
-
-```c
-// camera_control_service.c
-int camera_control_service_impl(const char* req, char* resp, size_t size)
-{
- /* Parse request, handle action, build response */
- snprintf(resp, size, "{\"result\":\"ok\"}");
- return 0;
-}
-```
-
-### Critical: Functions Must Be Non-Static
-
-Registry functions MUST NOT use `static`:
-
-```c
-/* WRONG - may be optimized away */
-static json_service_invoke_func_t android_static_service_lookup(...) { }
-
-/* CORRECT - global symbol preserved */
-json_service_invoke_func_t android_static_service_lookup(...) { }
-```
-
-Static functions have internal linkage and may be excluded from static archives.
-
-## Android vs Server-Side: Two `_invoke_json` Signatures
-
-The `axis2_json_rpc_msg_recv` framework dispatches to service functions
-using **different signatures** depending on the platform:
-
-### Android (2-param, `json_object*`)
-
-```c
-json_object* camera_control_service_invoke_json(
- const axutil_env_t *env,
- json_object *json_request);
-```
-
-- Resolved via the **static service registry** (`android_static_service_lookup`)
-- Uses weak/strong symbol linking — no `dlopen`/`dlsym` at runtime
-- Works with `json_object*` (json-c object pointers)
-- The Kanaha app's `axis2_static_service_adapter.c` bridges this to the
- app's `_impl()` functions which use plain `char*` strings
-
-### Server-side / Linux (4-param, `char*`)
-
-```c
-axis2_char_t* financial_benchmark_service_invoke_json(
- axis2_svc_t *svc,
- const axutil_env_t *env,
- const axis2_char_t *json_request,
- axis2_msg_ctx_t *msg_ctx);
-```
-
-- Resolved via `dlsym(RTLD_DEFAULT, "<serviceclass>_invoke_json")` after
- the service `.so` is loaded by `axutil_class_loader_create_dll()`
-- Has access to the `axis2_svc_t` and `axis2_msg_ctx_t` for operation
- routing, endpoint info, and MCP catalog generation
-- Works with `axis2_char_t*` (JSON strings), not `json_object*`
-
-### Why Two Signatures?
-
-Server-side services need `svc` and `msg_ctx` to:
-- Extract the operation name from the message context (URL path dispatch)
-- Access endpoint reference for MCP catalog intercepts
-- Get configuration context for multi-service deployments
-
-Android services don't need these because:
-- The static registry already routes by service name
-- Operations are identified by the `"action"` field in the JSON body
-- There is no multi-service deployment — one app, one service
-
-Services that want to work in both environments can export both:
-- `axis2_char_t* <serviceclass>_invoke_json(svc, env, json_str, msg_ctx)` for server-side
-- `axis2_char_t* <serviceclass>_process_json_only(env, json_str)` for simpler contexts
-
-## Linking for Android
-
-### Direct Linking Required
-
-Libtool strips `--whole-archive` flags, so direct linking is necessary:
-
-```bash
-$CC -fPIC -o httpd modules.o buildmark.o \
- -Wl,--export-dynamic \
- -L$DEPS/lib \
- # Apache modules... \
- $DEPS/lib/libmod_axis2.a \
- -Wl,--whole-archive $DEPS/lib/libaxis2_engine.a -Wl,--no-whole-archive \
- -Wl,--whole-archive libapp_services.a -Wl,--no-whole-archive \
- # Other Axis2 libraries... \
- -ljson-c -lnghttp2 -laprutil-1 -lapr-1 \
- -lm -llog -pthread
-```
-
-**Key flags:**
-
-| Flag | Purpose |
-|------|---------|
-| `-Wl,--whole-archive` | Include ALL objects from archive |
-| `-Wl,--no-whole-archive` | Resume normal archive linking |
-| `-Wl,--export-dynamic` | Export all symbols |
-| `-llog` | Android logging |
-
-### Verify Symbol Inclusion
-
-```bash
-nm httpd | grep -E "android_static|camera_control"
-
-# Expected (T = global text):
-# 00000000007b4998 T android_static_service_lookup
-# 00000000007b3ae0 T camera_control_service_invoke_json
-```
-
-## services.xml Configuration
-
-### REST Dispatcher Parameters
-
-Use `RESTLocation` and `RESTMethod`, NOT `httpPath` and `httpMethod`:
-
-```xml
-<service name="CameraControlService">
- <operation name="getStatus">
- <messageReceiver class="axis2_json_rpc_msg_recv"/>
- <parameter name="RESTMethod">POST</parameter>
- <parameter name="RESTLocation">/getStatus</parameter>
- <parameter name="contentType">application/json</parameter>
- </operation>
-</service>
-```
-
-## Adding New Services
-
-### Step 1: Add Weak Symbol to Axis2/C
-
-In `src/core/receivers/axis2_json_rpc_msg_recv.c`:
-
-```c
-#ifdef __ANDROID__
-__attribute__((weak))
-json_object* my_new_service_invoke_json(
- const axutil_env_t *env, json_object *json_request)
-{
- (void)env; (void)json_request;
- return NULL;
-}
-
-json_service_invoke_func_t android_static_service_lookup(const char *svc)
-{
- /* Existing services... */
- if (strcmp(svc, "MyNewService") == 0) {
- return my_new_service_invoke_json;
- }
- return NULL;
-}
-#endif
-```
-
-### Step 2: Implement in Application
-
-Create adapter and implementation files in your application, then link with `--whole-archive`.
-
-### Step 3: Create services.xml
-
-Deploy to `services/MyNewService/services.xml` in the Axis2 repository.
-
-## mod_axis2 Configuration
-
-### Android-Specific Initialization
-
-In `mod_axis2.c`, Android builds:
-- Skip shared memory (SELinux restrictions)
-- Use pool-based allocator
-- Run in single-process mode (`-X` flag)
-
-```c
-#ifdef __ANDROID__
- /* Skip shared memory, use pool allocation */
- allocator = apr_palloc(pconf, sizeof(axutil_allocator_t));
- goto engine_init;
-#endif
-```
-
-### Debug Logging
-
-Android logging uses INFO level:
-
-```c
-#ifdef __ANDROID__
-#include <android/log.h>
-#define AXIS2_ANDROID_LOG(fmt, ...) \
- __android_log_print(ANDROID_LOG_INFO, "Axis2-mod", fmt, ##__VA_ARGS__)
-#else
-#define AXIS2_ANDROID_LOG(fmt, ...) ((void)0)
-#endif
-```
-
-## HTTP/2 on Android
-
-### Single-Process Mode Considerations
-
-Apache runs with `-X` (single-process) on Android. For older devices, configure HTTP/2 conservatively:
-
-```apache
-# In http2-performance.conf
-H2MaxSessionStreams 1 # Single stream for stability
-H2StreamTimeout 30 # Prevent hanging streams
-H2WindowSize 65535 # Reduced memory
-H2Push off # Not needed for JSON-RPC
-H2MinWorkers 1
-H2MaxWorkers 2
-```
-
-## Android IPC Security
-
-### Native to Java Communication
-
-Axis2/C services on Android often need to communicate with Java application components. This is done via Android Intent broadcasts using the `am` (Activity Manager) command.
-
-### CRITICAL: Never Use system()
-
-**INSECURE (vulnerable to command injection):**
-
-```c
-/* NEVER DO THIS - user input is parsed by shell */
-char cmd[1024];
-snprintf(cmd, sizeof(cmd),
- "am broadcast --es pattern '%s'", user_pattern);
-system(cmd); /* Shell injection if pattern contains: '; rm -rf / ; ' */
-```
-
-**SECURE (use fork/execvp):**
-
-```c
-/* Arguments passed directly to process, no shell parsing */
-typedef struct {
- const char* type; /* "--es" for string, "--ei" for int */
- const char* key;
- const char* value;
-} intent_extra_t;
-
-static int send_intent_broadcast_secure(
- const char* component,
- const char* action,
- const intent_extra_t* extras,
- int num_extras)
-{
- /* Build argument array */
- int total_args = 8 + (num_extras * 3) + 1; /* base args + extras + NULL */
- char** argv = (char**)malloc(total_args * sizeof(char*));
-
- int i = 0;
- argv[i++] = "am";
- argv[i++] = "broadcast";
- argv[i++] = "--user";
- argv[i++] = "0";
- argv[i++] = "-n";
- argv[i++] = (char*)component;
- argv[i++] = "-a";
- argv[i++] = (char*)action;
-
- for (int j = 0; j < num_extras; j++) {
- argv[i++] = (char*)extras[j].type;
- argv[i++] = (char*)extras[j].key;
- argv[i++] = (char*)extras[j].value; /* User data - safe, not parsed */
- }
- argv[i] = NULL;
-
- pid_t pid = fork();
-
- if (pid < 0) {
- /* fork() failed — clean up and return error */
- free(argv);
- return -1;
- }
-
- if (pid == 0) {
- /* Child process */
- execvp("/system/bin/am", argv);
- _exit(127); /* exec failed */
- }
-
- /* Parent waits for child */
- free(argv);
-
- int status;
- if (waitpid(pid, &status, 0) < 0) {
- return -1;
- }
-
- return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : -1;
-}
-```
-
-### Why fork/execvp Is Secure
-
-| Approach | Shell Invoked | User Data Parsed | Injection Risk |
-|----------|---------------|------------------|----------------|
-| `system("cmd " + input)` | Yes | Yes | **HIGH** |
-| `popen("cmd " + input, "r")` | Yes | Yes | **HIGH** |
-| `fork()` + `execvp(argv[])` | No | No | **None** |
-
-With `execvp()`, arguments are passed as an array directly to the new process. Shell metacharacters (`'`, `;`, `|`, `$()`, etc.) are treated as literal characters, not interpreted.
-
-### Usage Example
-
-```c
-/* Safe: user-provided pattern passed directly to Java */
-int camera_device_delete_files_impl(const char* pattern, char* op_id)
-{
- intent_extra_t extras[] = {
- {"--es", "action", "delete_files"},
- {"--es", "pattern", pattern}, /* User input - safe */
- {"--es", "operation_id", op_id}
- };
-
- return send_intent_broadcast_secure(
- "org.kanaha.camera/org.kanaha.camera.CameraControlReceiver",
- "org.kanaha.CAMERA_CONTROL",
- extras, 3);
-}
-```
-
-### Java-Side Validation
-
-The Java `BroadcastReceiver` provides defense-in-depth validation:
-
-```java
-// In CameraControlReceiver.java
-class SecurityValidator {
- // Length limits
- static final int MAX_FILENAME_LENGTH = 255;
- static final int MAX_PATH_LENGTH = 1024;
-
- // Blocked patterns
- private static final String[] PATH_TRAVERSAL = {"..", "%2e%2e", "%00"};
- private static final String[] INJECTION = {"<script", "${", "`", "$("};
-
- static String validateFilenameOrPattern(String input, String paramName) {
- if (input == null || input.isEmpty()) return "empty " + paramName;
- if (input.length() > MAX_FILENAME_LENGTH) return paramName + " too long";
-
- // Check for path traversal
- String lower = input.toLowerCase();
- for (String pattern : PATH_TRAVERSAL) {
- if (lower.contains(pattern)) return "path traversal in " + paramName;
- }
-
- // Check for injection attempts
- for (String pattern : INJECTION) {
- if (lower.contains(pattern)) return "injection attempt in " + paramName;
- }
-
- // Whitelist allowed characters
- if (!input.matches("^[a-zA-Z0-9_\\-\\.\\*]+$")) {
- return "invalid characters in " + paramName;
- }
-
- return null; // Valid
- }
-}
-```
-
-### Security Headers Reference
-
-See the [Kanaha](https://github.com/robertlazarski/kanaha) project's `docs/SECURITY.md` for complete security documentation including:
-- mTLS configuration
-- Certificate revocation (CRL)
-- Audit logging
-- Apache httpd security headers
-
-**Note:** [Kanaha Camera](https://github.com/robertlazarski/kanaha) is an independent project (GPLv3 licensed due to OpenCamera
-dependency) and is not affiliated with Apache Software Foundation. It demonstrates
-production deployment patterns for the CameraControlService sample in
-`samples/user_guide/camera-control-service/`.
-
-**[Kanaha Audio](https://github.com/robertlazarski/kanaha-audio)** is a companion project (Apache 2.0 licensed)
-that demonstrates AudioSearchService — on-device ML inference via whisper.cpp,
-dispatched through the same weak symbol registry. Because all its dependencies
-are permissive (MIT + Apache 2.0), its patterns can flow upstream to Apache projects.
-
-### Registered Android Services
-
-| Service | Application | License | IPC Method |
-|---------|-------------|---------|------------|
-| CameraControlService | [Kanaha Camera](https://github.com/robertlazarski/kanaha) | GPL v3+ | fork/execvp Intent IPC (GPL boundary) |
-| AudioSearchService | [Kanaha Audio](https://github.com/robertlazarski/kanaha-audio) | Apache 2.0 | Direct function call (no GPL boundary) |
-
-## Summary Table
-
-| Aspect | Desktop/Server | Android |
-|--------|----------------|---------|
-| C standard | `-std=gnu99` | `-std=gnu99` |
-| Library type | Shared (.so) | Static (.a) |
-| Service loading | dlopen() | Weak symbol registry |
-| Service location | External .so | Application provides strong symbols |
-| Linker | Libtool | Direct clang with `--whole-archive` |
-| Apache mode | Multi-process | Single-process (`-X`) |
-| IPC method | N/A | fork()/execvp() or direct call |
-| Input validation | Application | Java SecurityValidator + C checks |
-
-## Files Reference
-
-Key files for Android support:
-
-**Axis2/C Core:**
-- `configure.ac` - Android detection, C flags
-- `src/core/receivers/axis2_json_rpc_msg_recv.c` - Static service registry (CameraControlService, AudioSearchService)
-- `src/core/transport/http/server/apache2/mod_axis2.c` - Android init, logging
-- `src/core/transport/http/server/apache2/apache2_worker.c` - Request processing
-
-**CameraControlService ([Kanaha Camera](https://github.com/robertlazarski/kanaha) — GPL v3+):**
-- `app/src/main/cpp/axis2c/camera_control_service.c` - Service impl with secure IPC
-- `app/src/main/java/org/kanaha/camera/CameraControlReceiver.java` - Java receiver with SecurityValidator
-- `app/src/main/assets/apache/httpd.conf` - Apache security config
-- `app/src/main/assets/apache/ssl.conf` - mTLS and CRL config
-- `docs/SECURITY.md` - Comprehensive security documentation
-
-**AudioSearchService ([Kanaha Audio](https://github.com/robertlazarski/kanaha-audio) — Apache 2.0):**
-- `app/src/main/cpp/axis2c/audio_search_service.c` - Service impl with direct whisper.cpp calls
-- `app/src/main/cpp/axis2c/axis2_static_service_adapter.c` - Strong symbol bridge to Axis2/C
-- `app/src/main/cpp/whisper/whisper_android_bridge.c` - whisper.cpp C API wrapper
-- `app/src/main/assets/axis2c/services/AudioSearchService/services.xml` - Service descriptor
-
----
-
-*Last updated: 2026-01-14*
+# Axis2/C Android Support
+
+This document describes how Axis2/C supports Android deployment with HTTP/2 and JSON-RPC.
+
+## Quick Reference for Claude
+
+When working with Axis2/C Android support, remember:
+
+1. **All builds use `-std=gnu99`** - Required for json-c header compatibility
+2. **All builds use `-fsigned-char`** - `axis2_char_t` is plain `char`, which is
+ signed on x86 but unsigned on ARM. See "Char signedness" below; an app that
+ compiles its own service sources must pass this flag too.
+3. **Android uses static linking** - No dlopen(), services use weak symbol registry
+4. **Service code lives in applications** - Axis2/C core only has weak symbol stubs
+5. **`--whole-archive` is required** - For weak/strong symbol resolution during linking
+6. **IPC uses fork()/execvp()** - NEVER use system() for security (command injection)
+7. **Java validates all input** - SecurityValidator class provides defense-in-depth
+
+## Build System
+
+### C Standard: Always gnu99
+
+All Axis2/C builds use `-std=gnu99` (GNU C99 mode). This is required because:
+
+1. **json-c headers** use C99 `inline` keyword
+2. **Android NDK headers** require GNU extensions
+3. **POSIX functions** are hidden by strict ANSI mode
+
+**Implementation in the root configure.ac:**
+
+```m4
+if test "$GCC" = "yes"; then
+ dnl Use gnu99 for json-c compatibility
+ CFLAGS="$CFLAGS -std=gnu99 -Wall -Wno-implicit-function-declaration -g -D_GNU_SOURCE"
+ CFLAGS="$CFLAGS -Wformat-security"
+ CFLAGS="$CFLAGS -Wsign-compare"
+ CFLAGS="$CFLAGS -fsigned-char"
+ CXXFLAGS="$CXXFLAGS -fsigned-char"
+fi
+```
+
+Seven configure.ac files carry their own `-std=gnu99` block:
+
+- `configure.ac` (root), `util/`, `axiom/`, `neethi/`, `guththila/`,
+ `samples/`, `tools/md5/`
+
+`samples/` builds with `-Werror`, so warning flags must not be added to it
+casually — a warning in a sample would fail the build rather than inform
+anyone. It gets `-fsigned-char` (a codegen flag, no warning risk) but not
+`-Wsign-compare`.
+
+**Each sub-package must set the hardening flags itself.** `util`, `axiom`,
+`neethi` and `guththila` are separate packages configured through
+`AC_CONFIG_SUBDIRS`, and they do **not** inherit `CFLAGS` additions the root
+configure makes to its own shell variable. A flag added only to the root
+configure.ac reaches `src/` and nothing else.
+
+This is easy to get wrong and hard to see, because a build with the flag missing
+succeeds and looks identical. Check the *generated Makefiles* rather than the
+build output:
+
+```bash
+for m in Makefile util/src/Makefile axiom/src/om/Makefile \
+ neethi/src/Makefile guththila/src/Makefile; do
+ printf "%-28s " "$m"
+ grep -m1 '^CFLAGS = ' "$m" | grep -q 'fsigned-char' && echo OK || echo MISSING
+done
+```
+
+Counting flagged compile lines in the make log is *not* a valid check: most
+compiles name their source relative to the current directory, so any
+per-directory grep silently samples a handful of lines and reports a clean
+result for a package that has none.
+
+When adding a compiler flag, add it to all five configure.ac files together.
+
+### Char signedness: why `-fsigned-char` is not optional
+
+`axis2_char_t` is `typedef char` — plain `char`, whose signedness is
+implementation-defined. It is **signed on x86-64 and unsigned on aarch64**.
+Axis2/C was written and is tested on x86, and it assumes the signed reading in
+places: there are `ctype` calls that pass an `axis2_char_t` straight to
+`isxdigit()` and friends without the `(unsigned char)` cast those macros
+require (for example `util/src/utils.c:543`). A byte with the high bit set
+reaches them as a negative int on one architecture and as 128-255 on the other.
+
+Without the flag the same source is a different program on the device than on
+the build machine, and any testing done on x86 stops transferring. With it, an
+x86 test run says something about arm64.
+
+Remove it only together with an audit of every `char` that crosses a `ctype`
+call or a comparison against a negative value.
+
+**Applications compiling their own service sources must pass it too.** The
+libraries are built with `-fsigned-char` via configure, but an app's own build
+script compiles its service `.c` files outside that, so without the flag the
+same `char` behaves differently on either side of the call:
+
+```bash
+$CC -fPIC -fsigned-char -D__ANDROID__ -c ... my_service.c -o my_service.o
+```
+
+### Android Detection
+
+Android builds are detected by the `--host` parameter:
+
+```m4
+AC_MSG_CHECKING([for Android target])
+case "$host" in
+ *-linux-android*|*-android*)
+ android_build=yes
+ AC_DEFINE([AXIS2_ANDROID_BUILD], [1], [Building for Android])
+ ;;
+ *)
+ android_build=no
+ ;;
+esac
+```
+
+When `android_build=yes`:
+- `AXIS2_ANDROID_BUILD` is defined
+- `__ANDROID__` is typically already defined by the NDK compiler
+- Static linking is expected
+
+### Android NDK Cross-Compilation
+
+**Environment setup:**
+
+```bash
+export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/28.0.12916984
+export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64
+export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang
+export AR=$TOOLCHAIN/bin/llvm-ar
+export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
+```
+
+**Configure command:**
+
+```bash
+./configure \
+ --host=aarch64-linux-android \
+ --prefix=$DEPS \
+ --enable-static \
+ --disable-shared \
+ --enable-http2 \
+ --enable-json \
+ --with-apr=$DEPS \
+ --with-openssl=$DEPS
+```
+
+**Convenience script:** Use `build_android.sh` in the repository root for a complete build:
+
+```bash
+./build_android.sh
+```
+
+This script sets up the toolchain, configures with all required flags, and builds. Edit the script to adjust paths for your NDK installation and dependency locations.
+
+Note it does not pass `--with-apache2`, so it does not produce `libmod_axis2.a`.
+A build intended for an app that embeds httpd needs that argument.
+
+### Cross-build hazards
+
+Three things that fail quietly. All three were observed, not theorised.
+
+**The dependency directory goes stale without saying so.** Apps link static
+archives from a prefix such as `$DEPS/lib`. Nothing there records which source
+revision produced them, and nothing warns when the tree is newer. A prefix
+populated months ago will link cleanly and produce a working APK that contains
+none of the fixes made since. After any refresh, **compare timestamps across
+every library**, not a sample — a partial refresh leaves a mixed-date directory
+that looks fine at a glance:
+
+```bash
+ls -la $DEPS/lib/libaxis2*.a $DEPS/lib/libaxutil.a | awk '{print $6, $7, $9}'
+```
+
+**Objects do not record the flags they were built with.** Cross-configuring a
+tree that already holds a native build does not force a rebuild of everything.
+Always `make distclean` before switching host, and confirm afterwards:
+
+```bash
+find . -name '*.o' | wc -l # expect 0 before the cross build starts
+```
+
+**An install step that globs for archives will copy host-built ones.** A line
+like `find . -name "*.a" -path "*/.libs/*" -exec cp {} $DEPS/lib/ \;` cannot
+tell an arm64 archive from an x86-64 one, and `samples/` in particular survives
+`distclean` in some configurations. Filter by architecture rather than by path:
+
+```bash
+for f in $(find . -name '*.a' -path '*/.libs/*'); do
+ ar p "$f" | file - | grep -q "ARM aarch64" && cp "$f" "$DEPS/lib/"
+done
+```
+
+### 16 KB page alignment (Android 15+)
+
+Devices with 16 KB pages will not load a shared library aligned for 4 KB. NDK
+r28 emits 16 KB-aligned `LOAD` segments by default; older NDKs need
+`-Wl,-z,max-page-size=16384`. Verify rather than assume:
+
+```bash
+readelf -lW libyour_httpd.so | awk '/LOAD/{print $NF}' | sort -u
+# want 0x4000 (16384); 0x1000 (4096) will fail to load on such a device
+```
+
+## Static Service Registry (Android)
+
+### The Problem
+
+Traditional Axis2/C loads services dynamically:
+
+```c
+void *lib = dlopen("libmyservice.so", RTLD_NOW);
+func = dlsym(lib, "service_invoke_json");
+```
+
+Android with static linking has no `.so` files to load.
+
+### Solution: Weak Symbol Architecture
+
+Axis2/C uses weak symbols so applications can provide service implementations at link time.
+
+**In `src/core/receivers/axis2_json_rpc_msg_recv.c`:**
+
+```c
+#ifdef __ANDROID__
+typedef json_object* (*json_service_invoke_func_t)(
+ const axutil_env_t *env, json_object *json_request);
+
+/* Weak symbol - returns NULL if no implementation linked */
+__attribute__((weak))
+json_object* camera_control_service_invoke_json(
+ const axutil_env_t *env, json_object *json_request)
+{
+ (void)env;
+ (void)json_request;
+ return NULL;
+}
+
+/* Lookup function - returns function pointer for known services */
+json_service_invoke_func_t
+android_static_service_lookup(const char *service_name)
+{
+ if (!service_name) return NULL;
+
+ if (strcmp(service_name, "CameraControlService") == 0) {
+ return camera_control_service_invoke_json;
+ }
+ if (strcmp(service_name, "AudioSearchService") == 0) {
+ return audio_search_service_invoke_json;
+ }
+ /* Add more services here */
+
+ return NULL;
+}
+#endif
+```
+
+### How It Works
+
+```
+[Axis2/C Framework] [Application]
+ | |
+ weak symbol: strong symbol:
+ camera_control_service_ camera_control_service_
+ invoke_json() -> NULL invoke_json() -> response
+ | |
+ +----------- Link Time ---------+
+ |
+ Strong symbol wins
+```
+
+1. **Framework compiles** with weak symbol stubs (return NULL)
+2. **Application provides** strong symbol implementations
+3. **At link time**, strong symbols override weak symbols
+4. **Result**: Application's service code handles requests
+
+### Application Service Implementation
+
+Applications provide two files:
+
+**Adapter (bridges Axis2/C to app):**
+
+```c
+// axis2_static_service_adapter.c
+#include <axutil_env.h>
+#include <json-c/json.h>
+
+extern int camera_control_service_impl(const char* req, char* resp, size_t size);
+
+/* Strong symbol overrides weak in Axis2/C */
+json_object* camera_control_service_invoke_json(
+ const axutil_env_t *env, json_object *json_request)
+{
+ const char *req_str = json_object_to_json_string(json_request);
+ char response[65536] = {0};
+
+ camera_control_service_impl(req_str, response, sizeof(response));
+
+ return json_tokener_parse(response);
+}
+```
+
+**Implementation (app-specific logic):**
+
+```c
+// camera_control_service.c
+int camera_control_service_impl(const char* req, char* resp, size_t size)
+{
+ /* Parse request, handle action, build response */
+ snprintf(resp, size, "{\"result\":\"ok\"}");
+ return 0;
+}
+```
+
+### Critical: Functions Must Be Non-Static
+
+Registry functions MUST NOT use `static`:
+
+```c
+/* WRONG - may be optimized away */
+static json_service_invoke_func_t android_static_service_lookup(...) { }
+
+/* CORRECT - global symbol preserved */
+json_service_invoke_func_t android_static_service_lookup(...) { }
+```
+
+Static functions have internal linkage and may be excluded from static archives.
+
+## Android vs Server-Side: Two `_invoke_json` Signatures
+
+The `axis2_json_rpc_msg_recv` framework dispatches to service functions
+using **different signatures** depending on the platform:
+
+### Android (2-param, `json_object*`)
+
+```c
+json_object* camera_control_service_invoke_json(
+ const axutil_env_t *env,
+ json_object *json_request);
+```
+
+- Resolved via the **static service registry** (`android_static_service_lookup`)
+- Uses weak/strong symbol linking — no `dlopen`/`dlsym` at runtime
+- Works with `json_object*` (json-c object pointers)
+- The Kanaha app's `axis2_static_service_adapter.c` bridges this to the
+ app's `_impl()` functions which use plain `char*` strings
+
+### Server-side / Linux (4-param, `char*`)
+
+```c
+axis2_char_t* financial_benchmark_service_invoke_json(
+ axis2_svc_t *svc,
+ const axutil_env_t *env,
+ const axis2_char_t *json_request,
+ axis2_msg_ctx_t *msg_ctx);
+```
+
+- Resolved via `dlsym(RTLD_DEFAULT, "<serviceclass>_invoke_json")` after
+ the service `.so` is loaded by `axutil_class_loader_create_dll()`
+- Has access to the `axis2_svc_t` and `axis2_msg_ctx_t` for operation
+ routing, endpoint info, and MCP catalog generation
+- Works with `axis2_char_t*` (JSON strings), not `json_object*`
+
+### Why Two Signatures?
+
+Server-side services need `svc` and `msg_ctx` to:
+- Extract the operation name from the message context (URL path dispatch)
+- Access endpoint reference for MCP catalog intercepts
+- Get configuration context for multi-service deployments
+
+Android services don't need these because:
+- The static registry already routes by service name
+- Operations are identified by the `"action"` field in the JSON body
+- There is no multi-service deployment — one app, one service
+
+Services that want to work in both environments can export both:
+- `axis2_char_t* <serviceclass>_invoke_json(svc, env, json_str, msg_ctx)` for server-side
+- `axis2_char_t* <serviceclass>_process_json_only(env, json_str)` for simpler contexts
+
+### The two signatures share one symbol name
+
+Read that carefully: both forms are called `<serviceclass>_invoke_json`. They
+are the same symbol with different parameter lists, so **one binary cannot
+contain both**. An Android link has to leave out whichever object exports the
+four-argument form, and link an adapter exporting the two-argument form
+instead.
+
+That exclusion has a consequence that is easy to miss. `axis2_get_instance` and
+`axis2_remove_instance` usually live in the same object as the four-argument
+entry point, and `axutil_class_loader_create_dll()` needs `axis2_get_instance`.
+Excluding the object removes both. So on a static Android build:
+
+- the class-loader path **cannot** serve the service — there is nothing to
+ `dlopen`, and the create function is gone with the excluded object;
+- the static registry is therefore not an optimisation, it is **the only route
+ in**.
+
+A service missing from `android_static_service_lookup()` does not fail loudly.
+It logs at INFO and falls through to dynamic loading, which then fails for its
+own reasons:
+
+```
+[JSON RPC MSG RECV] Android: Trying static service registry for 'X'
+[ANDROID_STATIC] Service 'X' not in static registry
+[JSON RPC MSG RECV] Android: Service 'X' not in static registry, trying dynamic loading
+```
+
+Seeing the third line in `logcat` means the registration is missing, not that
+the deployment is wrong. A registered service shows this instead:
+
+```
+[ANDROID_STATIC] Found service 'X' in static registry
+```
+
+### Verifying registration in a built binary
+
+Registry entries are easy to lose: a service can be registered in the engine an
+application happens to link against, while the entry is absent from the source
+tree. A clean rebuild then silently drops it, and the failure surfaces much
+later as "the service stopped working".
+
+The check does not need the device. Android links with `-Wl,--export-dynamic`,
+so `.dynsym` survives stripping:
+
+```sh
+# Is the application's strong symbol present? T = defined, W = only the weak stub
+nm -D libyour_httpd.so | grep '<serviceclass>_invoke_json'
+
+# Which names does the registry actually compare against?
+nm -D libyour_httpd.so | grep android_static_service_lookup # note the address
+llvm-objdump -d --start-address=<addr> --stop-address=<addr+0xc0> libyour_httpd.so
+```
+
+Count the `strcmp` calls in that disassembly — one per registered service. To
+name them, resolve each `adrp`/`add` pair to a virtual address and read the
+string out of `.rodata` at that offset (`readelf -S` gives the section address
+and file offset; the file offset of the string is
+`sh_offset + (vaddr - sh_addr)`).
+
+A `W` on the service symbol means only the weak stub was linked: the
+application's implementation is missing from the link, and every request will
+get a NULL response.
+
+## Linking for Android
+
+### Direct Linking Required
+
+Libtool strips `--whole-archive` flags, so direct linking is necessary:
+
+```bash
+$CC -fPIC -o httpd modules.o buildmark.o \
+ -Wl,--export-dynamic \
+ -L$DEPS/lib \
+ # Apache modules... \
+ $DEPS/lib/libmod_axis2.a \
+ -Wl,--whole-archive $DEPS/lib/libaxis2_engine.a -Wl,--no-whole-archive \
+ -Wl,--whole-archive libapp_services.a -Wl,--no-whole-archive \
+ # Other Axis2 libraries... \
+ -ljson-c -lnghttp2 -laprutil-1 -lapr-1 \
+ -lm -llog -pthread
+```
+
+**Key flags:**
+
+| Flag | Purpose |
+|------|---------|
+| `-Wl,--whole-archive` | Include ALL objects from archive |
+| `-Wl,--no-whole-archive` | Resume normal archive linking |
+| `-Wl,--export-dynamic` | Export all symbols |
+| `-llog` | Android logging |
+
+### Verify Symbol Inclusion
+
+```bash
+nm httpd | grep -E "android_static|camera_control"
+
+# Expected (T = global text):
+# 00000000007b4998 T android_static_service_lookup
+# 00000000007b3ae0 T camera_control_service_invoke_json
+```
+
+## services.xml Configuration
+
+### REST Dispatcher Parameters
+
+Use `RESTLocation` and `RESTMethod`, NOT `httpPath` and `httpMethod`:
+
+```xml
+<service name="CameraControlService">
+ <operation name="getStatus">
+ <messageReceiver class="axis2_json_rpc_msg_recv"/>
+ <parameter name="RESTMethod">POST</parameter>
+ <parameter name="RESTLocation">/getStatus</parameter>
+ <parameter name="contentType">application/json</parameter>
+ </operation>
+</service>
+```
+
+## Adding New Services
+
+### Step 1: Add Weak Symbol to Axis2/C
+
+In `src/core/receivers/axis2_json_rpc_msg_recv.c`:
+
+```c
+#ifdef __ANDROID__
+__attribute__((weak))
+json_object* my_new_service_invoke_json(
+ const axutil_env_t *env, json_object *json_request)
+{
+ (void)env; (void)json_request;
+ return NULL;
+}
+
+json_service_invoke_func_t android_static_service_lookup(const char *svc)
+{
+ /* Existing services... */
+ if (strcmp(svc, "MyNewService") == 0) {
+ return my_new_service_invoke_json;
+ }
+ return NULL;
+}
+#endif
+```
+
+**Both edits are required, and both belong in this repository.** A registry
+entry that exists only in a locally patched copy of the engine works until the
+next clean rebuild, then disappears without any build failing. If a service
+worked and then stopped after a dependency refresh, check the registry first.
+
+### Step 2: Implement in Application
+
+Create adapter and implementation files in your application, then link with `--whole-archive`.
+
+The adapter must export the **two-argument** form, and the link must exclude any
+object that exports the four-argument `<serviceclass>_invoke_json` — they are the
+same symbol name (see "The two signatures share one symbol name" above). For a
+service ported from a server-side sample this usually means compiling the
+sample's handler object but keeping it out of the final archive:
+
+```sh
+# handler.o is deliberately absent: it exports the four-argument _invoke_json,
+# which collides with the adapter's two-argument one
+$AR rcs libmyservice.a service.o adapter.o helpers.o
+```
+
+Verify afterwards with `nm -D` that the service symbol is `T` and not `W`
+(see "Verifying registration in a built binary" above).
+
+### Step 3: Create services.xml
+
+Deploy to `services/MyNewService/services.xml` in the Axis2 repository.
+
+## mod_axis2 Configuration
+
+### Android-Specific Initialization
+
+In `mod_axis2.c`, Android builds:
+- Skip shared memory (SELinux restrictions)
+- Use pool-based allocator
+- Run in single-process mode (`-X` flag)
+
+```c
+#ifdef __ANDROID__
+ /* Skip shared memory, use pool allocation */
+ allocator = apr_palloc(pconf, sizeof(axutil_allocator_t));
+ goto engine_init;
+#endif
+```
+
+### Debug Logging
+
+Android logging uses INFO level:
+
+```c
+#ifdef __ANDROID__
+#include <android/log.h>
+#define AXIS2_ANDROID_LOG(fmt, ...) \
+ __android_log_print(ANDROID_LOG_INFO, "Axis2-mod", fmt, ##__VA_ARGS__)
+#else
+#define AXIS2_ANDROID_LOG(fmt, ...) ((void)0)
+#endif
+```
+
+## HTTP/2 on Android
+
+### Single-Process Mode Considerations
+
+Apache runs with `-X` (single-process) on Android. For older devices, configure HTTP/2 conservatively:
+
+```apache
+# In http2-performance.conf
+H2MaxSessionStreams 1 # Single stream for stability
+H2StreamTimeout 30 # Prevent hanging streams
+H2WindowSize 65535 # Reduced memory
+H2Push off # Not needed for JSON-RPC
+H2MinWorkers 1
+H2MaxWorkers 2
+```
+
+## Android IPC Security
+
+### Native to Java Communication
+
+Axis2/C services on Android often need to communicate with Java application components. This is done via Android Intent broadcasts using the `am` (Activity Manager) command.
+
+### CRITICAL: Never Use system()
+
+**INSECURE (vulnerable to command injection):**
+
+```c
+/* NEVER DO THIS - user input is parsed by shell */
+char cmd[1024];
+snprintf(cmd, sizeof(cmd),
+ "am broadcast --es pattern '%s'", user_pattern);
+system(cmd); /* Shell injection if pattern contains: '; rm -rf / ; ' */
+```
+
+**SECURE (use fork/execvp):**
+
+```c
+/* Arguments passed directly to process, no shell parsing */
+typedef struct {
+ const char* type; /* "--es" for string, "--ei" for int */
+ const char* key;
+ const char* value;
+} intent_extra_t;
+
+static int send_intent_broadcast_secure(
+ const char* component,
+ const char* action,
+ const intent_extra_t* extras,
+ int num_extras)
+{
+ /* Build argument array */
+ int total_args = 8 + (num_extras * 3) + 1; /* base args + extras + NULL */
+ char** argv = (char**)malloc(total_args * sizeof(char*));
+
+ int i = 0;
+ argv[i++] = "am";
+ argv[i++] = "broadcast";
+ argv[i++] = "--user";
+ argv[i++] = "0";
+ argv[i++] = "-n";
+ argv[i++] = (char*)component;
+ argv[i++] = "-a";
+ argv[i++] = (char*)action;
+
+ for (int j = 0; j < num_extras; j++) {
+ argv[i++] = (char*)extras[j].type;
+ argv[i++] = (char*)extras[j].key;
+ argv[i++] = (char*)extras[j].value; /* User data - safe, not parsed */
+ }
+ argv[i] = NULL;
+
+ pid_t pid = fork();
+
+ if (pid < 0) {
+ /* fork() failed — clean up and return error */
+ free(argv);
+ return -1;
+ }
+
+ if (pid == 0) {
+ /* Child process */
+ execvp("/system/bin/am", argv);
+ _exit(127); /* exec failed */
+ }
+
+ /* Parent waits for child */
+ free(argv);
+
+ int status;
+ if (waitpid(pid, &status, 0) < 0) {
+ return -1;
+ }
+
+ return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : -1;
+}
+```
+
+### Why fork/execvp Is Secure
+
+| Approach | Shell Invoked | User Data Parsed | Injection Risk |
+|----------|---------------|------------------|----------------|
+| `system("cmd " + input)` | Yes | Yes | **HIGH** |
+| `popen("cmd " + input, "r")` | Yes | Yes | **HIGH** |
+| `fork()` + `execvp(argv[])` | No | No | **None** |
+
+With `execvp()`, arguments are passed as an array directly to the new process. Shell metacharacters (`'`, `;`, `|`, `$()`, etc.) are treated as literal characters, not interpreted.
+
+### Usage Example
+
+```c
+/* Safe: user-provided pattern passed directly to Java */
+int camera_device_delete_files_impl(const char* pattern, char* op_id)
+{
+ intent_extra_t extras[] = {
+ {"--es", "action", "delete_files"},
+ {"--es", "pattern", pattern}, /* User input - safe */
+ {"--es", "operation_id", op_id}
+ };
+
+ return send_intent_broadcast_secure(
+ "org.kanaha.camera/org.kanaha.camera.CameraControlReceiver",
+ "org.kanaha.CAMERA_CONTROL",
+ extras, 3);
+}
+```
+
+### Java-Side Validation
+
+The Java `BroadcastReceiver` provides defense-in-depth validation:
+
+```java
+// In CameraControlReceiver.java
+class SecurityValidator {
+ // Length limits
+ static final int MAX_FILENAME_LENGTH = 255;
+ static final int MAX_PATH_LENGTH = 1024;
+
+ // Blocked patterns
+ private static final String[] PATH_TRAVERSAL = {"..", "%2e%2e", "%00"};
+ private static final String[] INJECTION = {"<script", "${", "`", "$("};
+
+ static String validateFilenameOrPattern(String input, String paramName) {
+ if (input == null || input.isEmpty()) return "empty " + paramName;
+ if (input.length() > MAX_FILENAME_LENGTH) return paramName + " too long";
+
+ // Check for path traversal
+ String lower = input.toLowerCase();
+ for (String pattern : PATH_TRAVERSAL) {
+ if (lower.contains(pattern)) return "path traversal in " + paramName;
+ }
+
+ // Check for injection attempts
+ for (String pattern : INJECTION) {
+ if (lower.contains(pattern)) return "injection attempt in " + paramName;
+ }
+
+ // Whitelist allowed characters
+ if (!input.matches("^[a-zA-Z0-9_\\-\\.\\*]+$")) {
+ return "invalid characters in " + paramName;
+ }
+
+ return null; // Valid
+ }
+}
+```
+
+### Security Headers Reference
+
+See the [Kanaha](https://github.com/robertlazarski/kanaha) project's `docs/SECURITY.md` for complete security documentation including:
+- mTLS configuration
+- Certificate revocation (CRL)
+- Audit logging
+- Apache httpd security headers
+
+**Note:** [Kanaha Camera](https://github.com/robertlazarski/kanaha) is an independent project (GPLv3 licensed due to OpenCamera
+dependency) and is not affiliated with Apache Software Foundation. It demonstrates
+production deployment patterns for the CameraControlService sample in
+`samples/user_guide/camera-control-service/`.
+
+**[Kanaha Audio](https://github.com/robertlazarski/kanaha-audio)** is a companion project (Apache 2.0 licensed)
+that demonstrates AudioSearchService — on-device ML inference via whisper.cpp,
+dispatched through the same weak symbol registry. Because all its dependencies
+are permissive (MIT + Apache 2.0), its patterns can flow upstream to Apache projects.
+
+### Registered Android Services
+
+| Service | Application | License | IPC Method |
+|---------|-------------|---------|------------|
+| CameraControlService | [Kanaha Camera](https://github.com/robertlazarski/kanaha) | GPL v3+ | fork/execvp Intent IPC (GPL boundary) |
+| AudioSearchService | [Kanaha Audio](https://github.com/robertlazarski/kanaha-audio) | Apache 2.0 | Direct function call (no GPL boundary) |
+
+## Summary Table
+
+| Aspect | Desktop/Server | Android |
+|--------|----------------|---------|
+| C standard | `-std=gnu99` | `-std=gnu99` |
+| Library type | Shared (.so) | Static (.a) |
+| Service loading | dlopen() | Weak symbol registry |
+| Service location | External .so | Application provides strong symbols |
+| Linker | Libtool | Direct clang with `--whole-archive` |
+| Apache mode | Multi-process | Single-process (`-X`) |
+| IPC method | N/A | fork()/execvp() or direct call |
+| Input validation | Application | Java SecurityValidator + C checks |
+
+## Files Reference
+
+Key files for Android support:
+
+**Axis2/C Core:**
+- `configure.ac` - Android detection, C flags
+- `src/core/receivers/axis2_json_rpc_msg_recv.c` - Static service registry (CameraControlService, AudioSearchService)
+- `src/core/transport/http/server/apache2/mod_axis2.c` - Android init, logging
+- `src/core/transport/http/server/apache2/apache2_worker.c` - Request processing
+
+**CameraControlService ([Kanaha Camera](https://github.com/robertlazarski/kanaha) — GPL v3+):**
+- `app/src/main/cpp/axis2c/camera_control_service.c` - Service impl with secure IPC
+- `app/src/main/java/org/kanaha/camera/CameraControlReceiver.java` - Java receiver with SecurityValidator
+- `app/src/main/assets/apache/httpd.conf` - Apache security config
+- `app/src/main/assets/apache/ssl.conf` - mTLS and CRL config
+- `docs/SECURITY.md` - Comprehensive security documentation
+
+**AudioSearchService ([Kanaha Audio](https://github.com/robertlazarski/kanaha-audio) — Apache 2.0):**
+- `app/src/main/cpp/axis2c/audio_search_service.c` - Service impl with direct whisper.cpp calls
+- `app/src/main/cpp/axis2c/axis2_static_service_adapter.c` - Strong symbol bridge to Axis2/C
+- `app/src/main/cpp/whisper/whisper_android_bridge.c` - whisper.cpp C API wrapper
+- `app/src/main/assets/axis2c/services/AudioSearchService/services.xml` - Service descriptor
+
+---
+
+*Last updated: 2026-01-14*