Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

### Added

- Added `hostcalls::get_serialized_map` and `map::SerializedMap` for borrowing
header-map names and byte-valued payloads from a retained guest allocation,
with optional UTF-8 validation of values through `text_pairs()`.

## [0.2.5] - 2026-05-20

### Fixed
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,35 @@
- [Envoy filter metadata](./examples/envoy_filter_metadata/)
- [Envoy TCP Routing](./examples/envoy_tcp_routing/)

## Borrowed header maps

`hostcalls::get_serialized_map` retains the serialized guest allocation returned
by the host. `SerializedMap::pairs()` validates the map and returns a vector of
borrowed names and byte-valued payloads without copying each field.
`SerializedMap::text_pairs()` additionally validates values as UTF-8. Keep the
map owner alive while using either view; copy any values retained after it is
dropped, such as values needed by asynchronous callbacks.

```rust
use proxy_wasm::{hostcalls, types::MapType};

if let Ok(map) = hostcalls::get_serialized_map(MapType::HttpRequestHeaders) {
if let Ok(pairs) = map.pairs() {
for (name, value) in pairs {
// The views remain valid while ordinary host calls run.
let _ = hostcalls::set_map_value_bytes(
MapType::HttpRequestHeaders, name, Some(value),
);
}
}
}
```

The new accessors preserve order, duplicate names, empty fields, embedded NULs,
and payload bytes. Invalid buffers return `MapDecodeError` without exposing a
partial map. Names must be UTF-8; opaque values need not be. Existing owned APIs
and trait convenience methods are unchanged.

## Articles & blog posts from the community

- [Extending Envoy with WASM and Rust](https://antweiss.com/blog/extending-envoy-with-wasm-and-rust/)
Expand Down
163 changes: 145 additions & 18 deletions src/hostcalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use crate::dispatcher;
use crate::map::SerializedMap;
use crate::types::*;
use std::ptr::{null, null_mut};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
Expand Down Expand Up @@ -162,6 +163,37 @@ fn proxy_get_header_map_pairs(
mocks::proxy_get_header_map_pairs(map_type, return_map_data, return_map_size)
}

/// Acquires a serialized map without copying individual names or values.
///
/// The returned owner retains the host-transferred guest allocation. Use
/// `pairs()` for byte-valued views or `text_pairs()` for validated text views.
/// Host failures are returned unchanged. A null pointer with a nonzero size is
/// rejected as `InternalFailure`; a null, zero-length result is an empty map.
/// Existing owned map APIs retain their original behavior.
pub fn get_serialized_map(map_type: MapType) -> Result<SerializedMap, Status> {
let mut return_data: *mut u8 = null_mut();
let mut return_size: usize = 0;
unsafe {
match proxy_get_header_map_pairs(map_type, &mut return_data, &mut return_size) {
Status::Ok if return_data.is_null() => {
if return_size == 0 {
Ok(SerializedMap::default())
} else {
Err(Status::InternalFailure)
}
}
Status::Ok => {
// Same ownership boundary as get_map/get_map_bytes: the host used
// proxy_on_memory_allocate(size), transferring an exact-sized
// guest allocation. The Vec is its sole owner, including decode
// error paths; no references into mutable host storage are kept.
Ok(Vec::from_raw_parts(return_data, return_size, return_size).into())
}
status => Err(status),
}
}
}

pub fn get_map(map_type: MapType) -> Result<Vec<(String, String)>, Status> {
unsafe {
let mut return_data: *mut u8 = null_mut();
Expand Down Expand Up @@ -1198,24 +1230,43 @@ pub fn increment_metric(metric_id: u32, offset: i64) -> Result<(), Status> {
mod mocks {
use crate::hostcalls::utils::tests::SERIALIZED_MAP;
use crate::types::*;
use std::alloc::{Layout, alloc};
use std::cell::RefCell;

thread_local! {
static RESULT: RefCell<(Status, Option<Vec<u8>>, usize)> =
RefCell::new((Status::Ok, Some(SERIALIZED_MAP.to_vec()), 0));
}

pub fn set_result(status: Status, bytes: Option<Vec<u8>>, null_size: usize) {
RESULT.with(|result| *result.borrow_mut() = (status, bytes, null_size));
}

pub fn proxy_get_header_map_pairs(
_map_type: MapType,
return_map_data: *mut *mut u8,
return_map_size: *mut usize,
) -> Status {
let layout = Layout::array::<u8>(SERIALIZED_MAP.len()).unwrap();
unsafe {
*return_map_data = alloc(layout);
*return_map_size = SERIALIZED_MAP.len();
std::ptr::copy(
SERIALIZED_MAP.as_ptr(),
*return_map_data,
SERIALIZED_MAP.len(),
);
}
Status::Ok
RESULT.with(|result| {
let result = result.borrow();
if result.0 == Status::Ok {
unsafe {
if let Some(bytes) = &result.1 {
// Exercise the actual SDK allocator's ownership contract.
*return_map_data = crate::allocator::proxy_on_memory_allocate(bytes.len());
*return_map_size = bytes.len();
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
*return_map_data,
bytes.len(),
);
} else {
*return_map_data = std::ptr::null_mut();
*return_map_size = result.2;
}
}
}
result.0
})
}
}

Expand All @@ -1228,16 +1279,92 @@ mod tests {
#[global_allocator]
static ALLOCATOR: Mockalloc<System> = Mockalloc(System);

#[mockalloc::test]
#[test]
fn serialized_map_status_and_empty_buffers() {
use super::{get_serialized_map, mocks};
for status in [
Status::NotFound,
Status::BadArgument,
Status::InternalFailure,
] {
mocks::set_result(status, None, 0);
assert_eq!(
get_serialized_map(MapType::HttpRequestHeaders).unwrap_err(),
status
);
}
for bytes in [None, Some(vec![]), Some(vec![0; 4])] {
mocks::set_result(Status::Ok, bytes, 0);
assert!(
get_serialized_map(MapType::HttpRequestHeaders)
.unwrap()
.pairs()
.unwrap()
.is_empty()
);
}
mocks::set_result(Status::Ok, None, 4);
assert_eq!(
get_serialized_map(MapType::HttpRequestHeaders).unwrap_err(),
Status::InternalFailure
);
mocks::set_result(
Status::Ok,
Some(super::utils::tests::SERIALIZED_MAP.to_vec()),
0,
);
}

#[test]
fn serialized_map_allocations_and_drops() {
use super::{get_serialized_map, mocks, utils};
// Warm up the thread-local mock before measuring acquisition and decode.
for count in [1, 4, 20, 64] {
let entries = vec![("key", "value"); count];
mocks::set_result(Status::Ok, Some(utils::serialize_map(&entries)), 0);
for _ in 0..10 {
let info = mockalloc::record_allocs(|| {
let map = get_serialized_map(MapType::HttpRequestHeaders).unwrap();
assert_eq!(map.pairs().unwrap().len(), count);
});
info.result().unwrap();
assert_eq!(info.num_allocs(), 2);
}
}
for bytes in [vec![1], utils::serialize_map_bytes(&[("key", b"\xff")])] {
mocks::set_result(Status::Ok, Some(bytes), 0);
mockalloc::assert_allocs(|| {
let map = get_serialized_map(MapType::HttpRequestHeaders).unwrap();
assert!(map.text_pairs().is_err());
});
}
mocks::set_result(Status::Ok, Some(utils::tests::SERIALIZED_MAP.to_vec()), 0);
}

#[test]
fn test_get_map_no_leaks() {
let result = super::get_map(MapType::HttpRequestHeaders);
assert!(result.is_ok());
super::mocks::set_result(
Status::Ok,
Some(super::utils::tests::SERIALIZED_MAP.to_vec()),
0,
);
mockalloc::assert_allocs(|| {
let result = super::get_map(MapType::HttpRequestHeaders).unwrap();
assert!(!result.is_empty());
});
}

#[mockalloc::test]
#[test]
fn test_get_map_bytes_no_leaks() {
let result = super::get_map_bytes(MapType::HttpRequestHeaders);
assert!(result.is_ok());
super::mocks::set_result(
Status::Ok,
Some(super::utils::tests::SERIALIZED_MAP.to_vec()),
0,
);
mockalloc::assert_allocs(|| {
let result = super::get_map_bytes(MapType::HttpRequestHeaders).unwrap();
assert!(!result.is_empty());
});
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
extern crate test;

pub mod hostcalls;
pub mod map;
pub mod traits;
pub mod types;

Expand Down
Loading
Loading