Skip to content

[MEDIUM] Validate Struct backing memory during initialization - #1201

Open
OskarEichler wants to merge 1 commit into
ffi:masterfrom
OskarEichler:codex/security-validate-struct-memory-size
Open

OskarEichler wants to merge 1 commit into
ffi:masterfrom
OskarEichler:codex/security-validate-struct-memory-size

Conversation

@OskarEichler

Copy link
Copy Markdown

Summary

  • Reject a bounded memory object that is smaller than an FFI::Struct during construction.
  • Reuse the same validation for Struct#pointer= and remove its narrowing cast.
  • Leave deliberately unbounded raw-address pointers unchanged.

Issue

Struct#pointer= checks that the supplied memory covers the layout, but Struct#initialize(pointer) did not. A short MemoryPointer, Buffer, or bounded pointer could therefore back a larger struct.

Most field accessors perform their own bounds checks. Some whole-struct paths do not: Struct#initialize_copy copies the full layout size, and a struct returned by value from a callback is also copied at the declared native size. A short input can therefore cause an out-of-bounds read and copy adjacent process memory.

I classify this as medium urgency when an FFI wrapper maps untrusted, variable-length input into a fixed struct. Code that controls both the layout and pointer remains the common case, which limits exposure.

Reproduction

On ffi 1.17.4 and current master, construction accepts the undersized allocation:

require "ffi"

record = Class.new(FFI::Struct) do
  layout :value, :uint64
end

short = FFI::MemoryPointer.new(:char, 1)
record.new(short) # accepted before this change

Duplicating that object subsequently reaches an eight-byte memcpy from the one-byte allocation in struct_initialize_copy.

After this change, construction raises:

ArgumentError: memory of 1 bytes too small for struct ... (expected at least 8)

Relationship to #1193

#1193 adds checks to AbstractMemory#__copy_from__, including the nested-struct assignment caller. This change complements that sink check: it enforces the existing struct backing-memory invariant at construction and covers other whole-struct consumers, including duplication and callback returns.

JRuby already rejects an undersized struct at construction, as documented in #1193.

Verification

  • Native extension compiled on arm64 macOS with Ruby 4.0.6 and system libffi.
  • Exact 1.17.4 reproduction accepts the one-byte pointer; the patched extension raises before constructing the struct.
  • rbenv exec bundle exec rspec spec/ffi/struct_spec.rb: 148 examples, 0 failures.
  • rbenv exec bundle exec rspec: 5,063 examples, 0 failures.
  • git diff --check passes.

Limitations

  • Verification was performed on CRuby/macOS; existing CI provides the broader platform matrix.
  • Raw-address pointers retain LONG_MAX size and remain intentionally unchecked because FFI cannot infer their allocation extent.

Breaking changes

Code that intentionally constructs a struct over a bounded pointer shorter than its declared layout now receives ArgumentError. Such an object could not safely support all struct operations; this aligns construction with the existing pointer= behavior and JRuby.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant