Skip to content

Commit

Permalink
src: Add guarded heap allocations.
Browse files Browse the repository at this point in the history
I presume MaxSize is appropriate, but the libsodium code is a little confusing as the pointers make it look like the data and canary don't necessarily go in the same page. It's not great this is potentially variable as well.

AsSpan() is a bit dangerous because it can cause an access violation after Dispose() if you're not careful. It's best to call it each time rather than creating a span variable.

NoAccess(), ReadOnly(), and ReadWrite() might want to be renamed. For example, adding 'Access' to the end of each or adding 'Set' to the beginning of each.

sodium_allocarray() isn't supported because you can just slice the returned span. It just calls sodium_malloc() anyway.
  • Loading branch information
samuel-lucas6 committed Dec 8, 2024
1 parent d95c5bd commit b684510
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 0 deletions.
68 changes: 68 additions & 0 deletions src/Geralt.Tests/SecureMemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class SecureMemoryTests
public void Constants_Valid()
{
Assert.AreEqual(Environment.SystemPageSize, SecureMemory.PageSize);
Assert.AreEqual(Environment.SystemPageSize - 16, GuardedHeapAllocation.MaxSize);
}

[TestMethod]
Expand Down Expand Up @@ -110,4 +111,71 @@ public void LockMemory_UnlockAndZeroMemory_InvalidOperation()
Assert.ThrowsException<InvalidOperationException>(() => SecureMemory.UnlockAndZeroMemory(b));
}
}

[TestMethod]
public void GuardedHeapAllocation_Valid()
{
int size = ChaCha20.KeySize;
Span<byte> garbage = Enumerable.Repeat((byte)0xdb, size).ToArray();
Span<byte> copy = stackalloc byte[size];

using var secret = new GuardedHeapAllocation(size);
Span<byte> key = secret.AsSpan();
Assert.IsTrue(key.SequenceEqual(garbage));

RandomNumberGenerator.Fill(key);
Assert.IsFalse(key.SequenceEqual(garbage));

key.CopyTo(copy);
secret.ReadOnly();
Assert.IsTrue(key.SequenceEqual(copy));

secret.NoAccess();
// Can't check the value

secret.ReadWrite();
RandomNumberGenerator.Fill(key);
Assert.IsFalse(key.SequenceEqual(copy));
}

// This test has to be run manually, commenting out parts because there's no way to catch the access violation
/*[TestMethod]
public void GuardedHeapAllocation_Tampered()
{
var secret = new GuardedHeapAllocation(ChaCha20.KeySize);
Span<byte> key = secret.AsSpan();
secret.ReadOnly();
RandomNumberGenerator.Fill(key);
secret.NoAccess();
RandomNumberGenerator.Fill(key);
secret.Dispose();
RandomNumberGenerator.Fill(key);
}*/

[TestMethod]
[DataRow(0)]
[DataRow(4096)]
[DataRow(4096 - 15)]
public void GuardedHeapAllocation_Invalid(int size)
{
// This is the only exception that can be tested
Assert.ThrowsException<ArgumentOutOfRangeException>(() => new GuardedHeapAllocation(size));
}

[TestMethod]
public void GuardedHeapAllocation_Disposed()
{
var secret = new GuardedHeapAllocation(ChaCha20.KeySize);

secret.Dispose();

Assert.ThrowsException<ObjectDisposedException>(() => secret.AsSpan());
Assert.ThrowsException<ObjectDisposedException>(() => secret.NoAccess());
Assert.ThrowsException<ObjectDisposedException>(() => secret.ReadOnly());
Assert.ThrowsException<ObjectDisposedException>(() => secret.ReadWrite());
Assert.ThrowsException<ObjectDisposedException>(() => secret.Dispose());
}
}
57 changes: 57 additions & 0 deletions src/Geralt/Crypto/GuardedHeapAllocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using static Interop.Libsodium;

namespace Geralt;

public sealed class GuardedHeapAllocation : IDisposable
{
// A canary is placed before the data. However, this max size is artificial to limit memory usage
public static readonly int MaxSize = SecureMemory.PageSize - CANARY_SIZE;
private readonly IntPtr _pointer;
private readonly int _size;
private bool _disposed;

public GuardedHeapAllocation(int size)
{
Validation.SizeBetween(nameof(size), size, 1, MaxSize);
Sodium.Initialize();
_pointer = sodium_malloc((nuint)size);
if (_pointer == IntPtr.Zero) { throw new OutOfMemoryException("Unable to allocate memory."); }
_size = size;
_disposed = false;
}

public unsafe Span<byte> AsSpan()
{
if (_disposed) { throw new ObjectDisposedException(nameof(GuardedHeapAllocation)); }
return new Span<byte>((void*)_pointer, _size);
}

public void NoAccess()
{
if (_disposed) { throw new ObjectDisposedException(nameof(GuardedHeapAllocation)); }
int ret = sodium_mprotect_noaccess(_pointer);
if (ret != 0) { throw new InvalidOperationException("Unable to make memory inaccessible."); }
}

public void ReadOnly()
{
if (_disposed) { throw new ObjectDisposedException(nameof(GuardedHeapAllocation)); }
int ret = sodium_mprotect_readonly(_pointer);
if (ret != 0) { throw new InvalidOperationException("Unable to make memory read-only."); }
}

public void ReadWrite()
{
if (_disposed) { throw new ObjectDisposedException(nameof(GuardedHeapAllocation)); }
int ret = sodium_mprotect_readwrite(_pointer);
if (ret != 0) { throw new InvalidOperationException("Unable to make memory readable and writable."); }
}

public void Dispose()
{
if (_disposed) { throw new ObjectDisposedException(nameof(GuardedHeapAllocation)); }
// This calls sodium_mprotect_readwrite internally
sodium_free(_pointer);
_disposed = true;
}
}
22 changes: 22 additions & 0 deletions src/Geralt/Interop/Interop.SecureMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ internal static partial class Interop
{
internal static partial class Libsodium
{
internal const int CANARY_SIZE = 16;

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial void sodium_memzero(IntPtr pointer, nuint length);
Expand All @@ -16,5 +18,25 @@ internal static partial class Libsodium
[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int sodium_munlock(IntPtr pointer, nuint length);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial IntPtr sodium_malloc(nuint size);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial void sodium_free(IntPtr pointer);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int sodium_mprotect_noaccess(IntPtr pointer);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int sodium_mprotect_readonly(IntPtr pointer);

[LibraryImport(DllName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
internal static partial int sodium_mprotect_readwrite(IntPtr pointer);
}
}

0 comments on commit b684510

Please sign in to comment.