yams/plugins/object_storage_s3
trvon 93f2439471
build(plugins): export only the plugin C ABI; hide onnx_resource internals
Plugins and libyams_onnx_resource were linked with default visibility, so
their dynamic symbol tables exported everything they contained, including
statically linked third-party code. The S3 plugin exported 11,147
symbols (OpenSSL, libcurl, zlib, spdlog, nlohmann_json); the ONNX
resource library exported 895 (850 of them spdlog). Every loader uses
RTLD_LOCAL, so this was not a live interposition bug, but the export
table is the ABI and anything that binds RTLD_GLOBAL (or a future host)
could resolve into a plugin's private copies.

Plugins:
- plugins/exports/yams_plugin.map (ELF version script, global
  yams_plugin_*, local *) and yams_plugin.exp (ld64
  -exported_symbols_list) are applied to every plugin (s3, onnx, glint,
  zyp) with gnu_symbol_visibility: 'hidden' and --exclude-libs,ALL.
- The S3 legacy factory (yams_plugin_create/destroy_object_storage) is
  marked YAMS_PLUGIN_API so it stays exported under hidden visibility.

libyams_onnx_resource:
- Builds with hidden visibility; YAMS_ONNX_RESOURCE_API now expands to
  visibility("default") on GCC/Clang, so only OnnxConcurrencyRegistry is
  exported.
- It no longer links spdlog. Its messages reached the host log only
  because the executable's spdlog interposed the library's copy; with the
  copy hidden, the registry would have logged through a private default
  logger to stdout. The registry now takes a host log sink
  (OnnxConcurrencyRegistry::setLogSink); TuningManager and
  AbiModelProviderAdapter install a spdlog forwarder from the header-only
  onnx_registry_spdlog_sink.h. Without a sink, messages are dropped.

Tests (red first on grim, Debug):
- tests/plugins/plugin_export_surface_catch2_test.cpp dlopens the built
  S3 and zyp plugins and asserts the entry points resolve while libcurl,
  OpenSSL, zlib and zyp internals do not: 2/2 cases failed (11 of 31
  assertions) before, pass after.
- resource_governance_test gains a case for the log sink routing (did
  not compile before; passes after). `meson test --suite plugins`: 5/5.

Measured on grim (release, stripped staged payload):

  file                          bytes before  after       exports before  after
  libyams_object_storage_s3.so   7,854,064   6,240,112          11,147     10
  yams_zyp.so                       80,112      63,728             103      8
  libyams_onnx_resource.so         477,472      30,840             895     20
  bin/yams-cli                  33,712,056  33,859,512              80     39
  bin/yams-daemon               28,389,784  28,537,304              98     39

The executables now carry their own spdlog code paths instead of
exporting them for the resource library to interpose (+0.4%), and export
41 fewer symbols each. check_install_surface turns GREEN; budgets are
reset to the measured payload plus ~20%.

Signed-off-by: trvon <me@trevon.dev>
2026-10-01 18:51:34 -06:00
..
meson.build build(plugins): export only the plugin C ABI; hide onnx_resource internals 2026-10-01 18:51:34 -06:00
README.md build: standardize the Debug build dir on build/debug (#248) 2026-10-01 08:45:20 -06:00
s3_plugin.cpp build(plugins): export only the plugin C ABI; hide onnx_resource internals 2026-10-01 18:51:34 -06:00

S3 Object Storage Plugin (Experimental)

Overview

  • Provides an S3/R2-compatible object storage backend via the YAMS plugin system.
  • Implements basic object operations (PUT/GET/HEAD/DELETE) with AWS Signature V4.
  • Adds list() using S3 ListObjectsV2.
  • Includes a first-cut multipart upload (initiate/upload-part/complete/abort).

Status

  • Experimental: APIs and behavior may change. Not all S3 features are covered.
  • Multipart: first-cut implementation with optional checksum and SSE‑KMS headers.
  • list(): implements ListObjectsV2 with prefix and delimiter parsing; bounded pagination.

Provider validation status

  • Cloudflare R2: tested in this repository (smoke + storage benchmark path).
  • AWS S3: supported by the same S3-compatible plugin/configuration path, but not currently validated by automated tests in this repository.

Configuration

  • BackendConfig.type: "s3"
  • BackendConfig.url: "s3:///"
  • BackendConfig.region: AWS region (e.g., "us-east-1"), or "auto" for R2.
  • BackendConfig.usePathStyle: true to force path-style addressing.
  • Credentials (config.credentials map or environment):
    • access_key / secret_key / session_token
    • endpoint: custom endpoint host (e.g., ".r2.cloudflarestorage.com")

Environment Variables

  • AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN
  • AWS_REGION (used by the smoke test; region is otherwise taken from BackendConfig)
  • S3_TEST_BUCKET, S3_TEST_ENDPOINT, S3_TEST_USE_PATH_STYLE=1 (smoke test)

Build Notes

  • Requires libcurl and OpenSSL development libraries present at build time.
  • Target name: yams_object_storage_s3

Testing

  • Unit coverage: meson test -C build/debug storage_submodule --print-errorlogs (includes signer and storage submodule tests).
  • Optional network smoke test: meson test -C build/debug s3_plugin_smoke --print-errorlogs.
  • The smoke test requires the environment variables above and performs a PUT/GET/HEAD/LIST/DELETE round-trip.
  • Helper script: scripts/dev/run_s3_plugin_smoke.sh [build/debug].

Example (R2)

export AWS_ACCESS_KEY_ID="<r2-access-key>"
export AWS_SECRET_ACCESS_KEY="<r2-secret-key>"
export AWS_REGION="auto"
export S3_TEST_BUCKET="<bucket>"
export S3_TEST_ENDPOINT="<accountid>.r2.cloudflarestorage.com"
export S3_TEST_USE_PATH_STYLE=0
meson test -C build/debug s3_plugin_smoke --print-errorlogs

Example (AWS S3, supported path)

export AWS_ACCESS_KEY_ID="<aws-access-key-id>"
export AWS_SECRET_ACCESS_KEY="<aws-secret-access-key>"
export AWS_REGION="us-east-1"
export S3_TEST_BUCKET="<bucket>"
export S3_TEST_ENDPOINT="s3.us-east-1.amazonaws.com"
export S3_TEST_USE_PATH_STYLE=0
meson test -C build/debug s3_plugin_smoke --print-errorlogs

Note: set S3_TEST_ENDPOINT to host only. Do not include https:// or /<bucket>. Note: Cloudflare API bearer tokens are not S3 credentials; use an R2 S3 Access Key ID + Secret Access Key pair from R2 -> Manage R2 API tokens.

If you only have a Cloudflare API token, create temporary S3 credentials via POST /accounts/{account_id}/r2/temp-access-credentials and export returned accessKeyId, secretAccessKey, and sessionToken as AWS env vars.

Security

  • Credentials are taken from config and/or standard AWS environment variables.
  • Signing uses AWS SigV4; verify environment and endpoint correctness for non-AWS providers.

Enhancements in this slice

  • Stronger URI encoding for keys and canonicalization.
  • Prefix-aware listing with delimiter support using XML parsing of ListObjectsV2.
  • Optional checksum/SSE headers for PUT and multipart flows (signed):
    • checksumAlgorithm = "sha256" adds x-amz-checksum-sha256.
    • sseKmsKeyId adds x-amz-server-side-encryption: aws:kms and key id.
    • storageClass sets x-amz-storage-class.

Prefix-aware listing and prefixes

  • Core API (IStorageBackend::list): returns a flat list of keys for a given prefix.
  • Plugin API (object_storage_v1 via adapter): accepts delimiter, pageToken, maxKeys in opts_json and returns JSON:
    • { "items": ["key1", ...], "nextPageToken": "...", "prefixes": ["dir1/", ...] }.
    • Not all backends populate prefixes; clients can infer prefixes from keys when unset.

Signer header coverage

  • The SigV4 signer includes optional headers in the signed set when provided (SSE‑KMS, storage class, checksum).

Limitations / Future Work

  • Canonical query/signing consolidation across signer and callers.
  • SSE‑C, CRC32C, compose, versioning, presign URLs, and robust pagination APIs.
  • DR presence and replication hooks aligned with object_storage_v1 interface.