Skip to content

Commit 1648605

Browse files
p1024kclaude
andcommitted
fix: pad SecureBuffer data to 16-byte boundary on Windows
Windows CryptProtectMemory API requires memory regions to be aligned to 16-byte boundaries. This commit modifies SecureBuffer to automatically pad data to meet this requirement while preserving the logical length. Changes: - Add `logical_len` field to track actual data length excluding padding - Pad data to 16-byte boundary on Windows before calling protect_memory - Update as_slice() to return only logical data (without padding) - Update into_inner() to truncate padding before returning - Update Clone implementation to work with logical data Fixes test failures on Windows: - mcp::secure_memory::tests::test_secure_buffer_creation (3 bytes) - mcp::secure_memory::tests::test_secure_buffer_as_slice (3 bytes) - mcp::secure_memory::tests::test_secure_buffer_clone (3 bytes) - mcp::secure_memory::tests::test_secure_buffer_into_inner (3 bytes) - mcp::executors::git::tests::test_set_credentials_clears_ssh (8 bytes) - mcp::executors::ssh_executor::tests::test_ssh_executor_creation (8 bytes) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 22a18f8 commit 1648605

1 file changed

Lines changed: 55 additions & 8 deletions

File tree

src/mcp/secure_memory.rs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@ impl From<PlatformError> for SecureMemoryError {
4141
/// # Security
4242
///
4343
/// - On Unix: Uses mlock() to prevent memory from being swapped to disk
44-
/// - On Windows: Uses CryptProtectMemory to encrypt memory
44+
/// - On Windows: Uses CryptProtectMemory to encrypt memory (data is padded to 16-byte boundaries)
4545
/// - Automatically zeroizes on drop
4646
///
47+
/// # Platform Notes
48+
///
49+
/// On Windows, CryptProtectMemory requires memory to be aligned to 16-byte boundaries.
50+
/// SecureBuffer automatically pads data to meet this requirement. The logical length
51+
/// (the actual data length) is preserved and returned by `len()` and `as_slice()`.
52+
///
4753
/// # Example
4854
///
4955
/// ```no_run
@@ -59,9 +65,12 @@ impl From<PlatformError> for SecureMemoryError {
5965
/// // Buffer is automatically zeroized and unprotected on drop
6066
/// ```
6167
pub struct SecureBuffer {
62-
/// The protected data
68+
/// The protected data (may be padded on Windows for 16-byte alignment)
6369
data: Vec<u8>,
6470

71+
/// The actual logical length of the data (excluding padding)
72+
logical_len: usize,
73+
6574
/// Whether memory is currently protected
6675
is_protected: bool,
6776
}
@@ -82,20 +91,39 @@ impl SecureBuffer {
8291
/// Returns an error if:
8392
/// - Memory protection fails (e.g., mlock fails due to resource limits)
8493
/// - Data pointer is null
94+
///
95+
/// # Platform Notes
96+
///
97+
/// On Windows, data is automatically padded to meet CryptProtectMemory's
98+
/// 16-byte alignment requirement. The logical length is preserved.
8599
pub fn new(mut data: Vec<u8>) -> Result<Self, SecureMemoryError> {
100+
let logical_len = data.len();
101+
86102
if data.is_empty() {
87103
return Ok(Self {
88104
data,
105+
logical_len: 0,
89106
is_protected: false,
90107
});
91108
}
92109

110+
// On Windows, pad data to 16-byte boundary for CryptProtectMemory
111+
#[cfg(target_os = "windows")]
112+
{
113+
const BLOCK_SIZE: usize = 16;
114+
let padding_needed = (BLOCK_SIZE - (data.len() % BLOCK_SIZE)) % BLOCK_SIZE;
115+
if padding_needed != 0 {
116+
data.extend(vec![0u8; padding_needed]);
117+
}
118+
}
119+
93120
// Protect the memory
94121
protect_memory(data.as_mut_ptr(), data.len())
95122
.map_err(|e| SecureMemoryError::ProtectionFailed(e.to_string()))?;
96123

97124
Ok(Self {
98125
data,
126+
logical_len,
99127
is_protected: true,
100128
})
101129
}
@@ -116,22 +144,26 @@ impl SecureBuffer {
116144
///
117145
/// The data remains protected while you have a reference to it.
118146
/// On Windows, the data is encrypted and will be decrypted on access.
147+
/// Only the logical data (excluding padding) is returned.
119148
pub fn as_slice(&self) -> &[u8] {
120-
&self.data
149+
&self.data[..self.logical_len]
121150
}
122151

123152
/// Unprotect the memory and return the underlying data
124153
///
125154
/// This consumes the SecureBuffer and returns the raw Vec<u8>.
126155
/// The caller is responsible for zeroizing the data after use.
156+
/// On Windows, padding is removed before returning.
127157
pub fn into_inner(mut self) -> Vec<u8> {
128158
if self.is_protected {
129159
// Unprotect before returning
130160
let _ = unprotect_memory(self.data.as_mut_ptr(), self.data.len());
131161
self.is_protected = false;
132162
}
133-
// Use std::mem::take to avoid moving out of type with Drop
134-
std::mem::take(&mut self.data)
163+
// Take the data and truncate to logical length
164+
let mut data = std::mem::take(&mut self.data);
165+
data.truncate(self.logical_len);
166+
data
135167
}
136168
}
137169

@@ -148,11 +180,11 @@ impl Drop for SecureBuffer {
148180

149181
impl Clone for SecureBuffer {
150182
fn clone(&self) -> Self {
151-
// Create a new buffer with cloned data
152-
// The new buffer will also be protected
153-
let cloned_data = self.data.clone();
183+
// Clone only the logical data (without padding)
184+
let cloned_data = self.as_slice().to_vec();
154185
Self::new(cloned_data).unwrap_or_else(|_| Self {
155186
data: vec![],
187+
logical_len: 0,
156188
is_protected: false,
157189
})
158190
}
@@ -214,4 +246,19 @@ mod tests {
214246
let buffer = buffer.unwrap();
215247
assert_eq!(buffer.len(), 1024);
216248
}
249+
250+
#[test]
251+
fn test_secure_buffer_non_16_byte_sizes() {
252+
// Test various sizes that are not multiples of 16
253+
// This ensures Windows padding works correctly
254+
for size in [1, 3, 5, 7, 8, 11, 13, 15] {
255+
let data = vec![0x42u8; size];
256+
let buffer = SecureBuffer::new(data.clone());
257+
258+
assert!(buffer.is_ok(), "Failed for size {}", size);
259+
let buffer = buffer.unwrap();
260+
assert_eq!(buffer.len(), size, "Length mismatch for size {}", size);
261+
assert_eq!(buffer.as_slice(), data.as_slice(), "Data mismatch for size {}", size);
262+
}
263+
}
217264
}

0 commit comments

Comments
 (0)