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
2 changes: 2 additions & 0 deletions libopencas/libopencas.c
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ static int nl_resolve_family(struct nl_ctx *ctx, const char *name)
genl->version = 1;

nla = (struct nlattr *)((char *)buf + nlh->nlmsg_len);
if (strlen(name) + 1 > sizeof(buf) - nlh->nlmsg_len - NLA_HDRLEN)
return -EINVAL;
nla->nla_type = CTRL_ATTR_FAMILY_NAME;
nla->nla_len = NLA_HDRLEN + strlen(name) + 1;
memcpy(nla_data(nla), name, strlen(name) + 1);
Expand Down
250 changes: 250 additions & 0 deletions tests/test_invariant_libopencas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import pytest
import ctypes
import struct


# Simulated NLA buffer size constraint (typical netlink attribute max)
NLA_MAX_SIZE = 256
NLA_HEADER_SIZE = 4 # nla_len (2 bytes) + nla_type (2 bytes)
NLA_DATA_MAX = NLA_MAX_SIZE - NLA_HEADER_SIZE


def simulate_nla_copy(name: str, buffer_size: int = NLA_DATA_MAX) -> bool:
"""
Simulates the vulnerable memcpy pattern:
memcpy(nla_data(nla), name, strlen(name) + 1)

Returns True if the copy is safe (fits within buffer), False if overflow would occur.
This function models the SECURE version of the operation that MUST validate length.
"""
if not isinstance(name, str):
return False

# strlen(name) + 1 includes null terminator
required_size = len(name.encode('utf-8')) + 1

# Security invariant: copy size must not exceed allocated buffer
if required_size > buffer_size:
return False # Would overflow - must be rejected

# Simulate the actual copy into a fixed-size buffer
try:
buf = bytearray(buffer_size)
encoded = name.encode('utf-8') + b'\x00'
if len(encoded) > buffer_size:
return False
buf[:len(encoded)] = encoded
return True
except (ValueError, OverflowError, MemoryError):
return False


def safe_nla_name_copy(name: str, buffer_size: int = NLA_DATA_MAX) -> bytes:
"""
Secure implementation that MUST validate name length before copy.
Raises ValueError if name would overflow the NLA buffer.
"""
if not isinstance(name, str):
raise TypeError("name must be a string")

encoded = name.encode('utf-8')
required = len(encoded) + 1 # +1 for null terminator

if required > buffer_size:
raise ValueError(
f"Name length {required} exceeds NLA buffer size {buffer_size}: "
f"potential buffer overflow"
)

buf = bytearray(buffer_size)
buf[:len(encoded)] = encoded
buf[len(encoded)] = 0 # null terminator
return bytes(buf)


@pytest.mark.parametrize("payload", [
# Exact boundary - should succeed
"A" * (NLA_DATA_MAX - 1),
# One byte over boundary - must be rejected
"A" * NLA_DATA_MAX,
# Massively oversized name
"A" * 1024,
"B" * 4096,
"C" * 65535,
# Null bytes embedded (potential bypass attempts)
"evil\x00" + "A" * 300,
# Unicode/multibyte characters that expand when encoded
"\u00e9" * 200, # é = 2 bytes each in UTF-8
"\u4e2d" * 100, # Chinese char = 3 bytes each in UTF-8
"\U0001f600" * 70, # Emoji = 4 bytes each in UTF-8
# Format string-like payloads
"%s" * 200,
"%n%n%n%n%n" * 50,
# Path traversal / injection attempts
"../" * 100,
"/etc/passwd" + "A" * 300,
# Shell metacharacters
"$()" * 100,
"`id`" * 100,
# Very long single token
"x" * (NLA_MAX_SIZE * 2),
"x" * (NLA_MAX_SIZE * 10),
# Empty string (edge case - should succeed)
"",
# Single character (should succeed)
"a",
# Exactly at limit
"Z" * (NLA_DATA_MAX - 1),
# One over limit
"Z" * NLA_DATA_MAX,
# Two over limit
"Z" * (NLA_DATA_MAX + 1),
# Whitespace-heavy names
" " * 500,
"\t" * 500,
"\n" * 500,
# Mixed adversarial
"A" * 100 + "\x00" + "B" * 200,
"\xff" * 300,
"\x7f" * 300,
])
def test_nla_name_copy_no_buffer_overflow(payload):
"""
Invariant: A name string copied into a netlink attribute (NLA) buffer
must NEVER exceed the allocated buffer size. The copy operation must
validate that strlen(name) + 1 <= buffer_size before performing the copy.
Any name that would cause strlen(name) + 1 > buffer_size MUST be rejected
with an error, never silently overflowing the buffer.
"""
buffer_size = NLA_DATA_MAX

# Determine if this payload would overflow
try:
encoded_len = len(payload.encode('utf-8'))
except (UnicodeEncodeError, AttributeError):
encoded_len = len(payload) if isinstance(payload, bytes) else 0

required_size = encoded_len + 1 # +1 for null terminator
would_overflow = required_size > buffer_size

if would_overflow:
# INVARIANT: Oversized names MUST raise an error, never silently overflow
with pytest.raises((ValueError, OverflowError, MemoryError, TypeError)):
safe_nla_name_copy(payload, buffer_size)

# Also verify the simulation correctly identifies this as unsafe
result = simulate_nla_copy(payload, buffer_size)
assert result is False, (
f"Security violation: name of encoded length {encoded_len} "
f"(+1 null = {required_size}) must not fit in buffer of size {buffer_size}. "
f"Buffer overflow would occur."
)
else:
# INVARIANT: Valid-sized names must be accepted and copied correctly
result = safe_nla_name_copy(payload, buffer_size)
assert isinstance(result, bytes), "Copy must return bytes"
assert len(result) == buffer_size, "Buffer must be exactly buffer_size bytes"

# Verify null termination
encoded = payload.encode('utf-8')
assert result[len(encoded)] == 0, "Copied name must be null-terminated"

# Verify no data written beyond buffer bounds
assert len(result) <= buffer_size, (
f"Result length {len(result)} must not exceed buffer size {buffer_size}"
)


@pytest.mark.parametrize("name,buffer_size,should_succeed", [
# Exact fit: name + null == buffer_size
("A" * (NLA_DATA_MAX - 1), NLA_DATA_MAX, True),
# One byte too large
("A" * NLA_DATA_MAX, NLA_DATA_MAX, False),
# Empty string always fits
("", NLA_DATA_MAX, True),
# Tiny buffer
("hello", 4, False), # 6 bytes needed, 4 available
("hi", 4, False), # 3 bytes needed, 4 available -> fits
("hi", 3, True), # 3 bytes needed, 3 available -> fits exactly
("hi", 2, False), # 3 bytes needed, 2 available -> overflow
# Large buffer, large name
("A" * 1000, 2048, True),
("A" * 2048, 2048, False), # 2049 needed, 2048 available
])
def test_nla_boundary_conditions(name, buffer_size, should_succeed):
"""
Invariant: The NLA buffer copy must correctly enforce boundary conditions.
Names that fit (strlen + 1 <= buffer_size) must succeed.
Names that don't fit must be rejected to prevent buffer overflow.
"""
if should_succeed:
result = safe_nla_name_copy(name, buffer_size)
assert result is not None
assert len(result) == buffer_size
encoded = name.encode('utf-8')
assert result[:len(encoded)] == encoded
assert result[len(encoded)] == 0 # null terminator present
else:
with pytest.raises((ValueError, OverflowError)):
safe_nla_name_copy(name, buffer_size)


def test_nla_copy_never_writes_beyond_buffer():
"""
Invariant: Under no circumstances should the copy operation write
data beyond the end of the allocated NLA buffer. This is the core
security property preventing the buffer overflow vulnerability.
"""
buffer_size = NLA_DATA_MAX

# Test with a range of sizes around the boundary
for extra in range(0, 100):
name_len = buffer_size - 1 + extra # starts at exact fit, goes over
name = "X" * name_len
required = name_len + 1 # +1 for null terminator

if required <= buffer_size:
# Must succeed without overflow
result = safe_nla_name_copy(name, buffer_size)
assert len(result) == buffer_size, (
f"Buffer size must be exactly {buffer_size}, got {len(result)}"
)
else:
# Must be rejected
with pytest.raises((ValueError, OverflowError)):
safe_nla_name_copy(name, buffer_size)


def test_nla_copy_rejects_all_oversized_names():
"""
Invariant: Every name whose strlen(name) + 1 > NLA_DATA_MAX
must be unconditionally rejected. No oversized name should ever
result in a successful copy that could overflow the buffer.
"""
oversized_names = [
"A" * NLA_DATA_MAX, # exactly 1 byte over (null terminator)
"B" * (NLA_DATA_MAX + 1), # 2 bytes over
"C" * (NLA_DATA_MAX * 2), # double the limit
"D" * 65536, # very large
"\xff" * NLA_DATA_MAX, # binary data, oversized
]

for name in oversized_names:
encoded_len = len(name.encode('utf-8', errors='replace'))
required = encoded_len + 1

assert required > NLA_DATA_MAX, (
f"Test setup error: name should be oversized but required={required} "
f"<= buffer={NLA_DATA_MAX}"
)

with pytest.raises((ValueError, OverflowError, MemoryError)):
safe_nla_name_copy(name, NLA_DATA_MAX)

# Simulation must also flag this as unsafe
is_safe = simulate_nla_copy(name, NLA_DATA_MAX)
assert is_safe is False, (
f"SECURITY VIOLATION: Oversized name (encoded_len={encoded_len}, "
f"required={required}) was not flagged as unsafe. "
f"This would cause a buffer overflow in the NLA copy operation."
)