binaryninja-api icon indicating copy to clipboard operation
binaryninja-api copied to clipboard

Empty class declarations don't correctly propagate inherited fields to subclasses

Open WeiN76LQh opened this issue 1 year ago • 0 comments

Version and Platform (required):

  • Binary Ninja Version: 4.1.5902 (f2165c5d) and 4.2.6168-dev (cb0f14d8)
  • OS: macOS
  • OS Version: 15.0
  • CPU Architecture: M1

Bug Description: Wasn't sure how exactly to title this issue.

Given 3 classes; foo, bar and baz, where baz is a subclass of bar and bar is a subclass of foo. If bar is empty, aside from fields inherited from foo, those inherited fields will be overwritten by fields declared in baz.

Steps To Reproduce: Use the Create Types from C Source window and paste in the following type definitions all together:

class foo {
	int64_t field1;
	int64_t field2;
};

class __base(foo, 0) bar {
};

class __base(bar, 0) baz {
	int64_t field3;
};

The following 3 types should be created in the user types section:

class foo
{
    int64_t field1;
    int64_t field2;
};

class __base(foo, 0) bar
{
    __inherited int64_t foo::field1;
    __inherited int64_t foo::field2;
};

class __base(bar, 0) baz
{
    int64_t field3;
    __inherited int64_t foo::field2;
};

Notice how baz has field3 at offset 0 and has overwritten foo::field1.

Expected Behavior: field3 for baz should be after the 2 inherited fields from foo, and not overlap/overwrite foo::field1. These are the types I expect to be created:

class foo
{
    int64_t field1;
    int64_t field2;
};

class __base(foo, 0) bar
{
    __inherited int64_t foo::field1;
    __inherited int64_t foo::field2;
};

class __base(bar, 0) baz
{
    __inherited int64_t foo::field1;
    __inherited int64_t foo::field2;
    int64_t field3;
};

Additional Information: I've noticed that creating an empty class type like bar, that inherits from a class with fields, using the API StructureBuilder, results in types like bar showing as having a 0 size in the user types list. Also inspecting the StructureBuilder.width using the API will report a width of 0. As soon as a field is added to the bar StructureBuilder its size will suddenly jump from 0 to the actual size it should be (size of inherited fields plus the field just added). So it seems the issue lies with the fact that internally Binary Ninja seems to think empty classes that inherit from another class are 0 sized even if they inherit fields.

Running the following Python code in the Binary Ninja intepreter should show this:

# Create a base type `foo`
foo_builder = binaryninja.StructureBuilder.create()
foo_builder.append(binaryninja.Type.int(8), "field1")
foo_builder.append(binaryninja.Type.int(8), "field2")
bv.define_user_type("foo", foo_builder.immutable_copy())

# Create a new type `bar` that inherits from `foo`
bar_builder = binaryninja.StructureBuilder.create()
bar_builder.base_structures = [ binaryninja.BaseStructure(bv.types["foo"], 0) ]
print("bar_builder.width =", bar_builder.width)
bv.define_user_type("bar", bar_builder.immutable_copy())
print("sizeof(bar) =",bv.types["bar"].width)

# Show that the width updates correctly once a field is added
bar_builder.append(binaryninja.Type.int(8), "field3")
print("bar_builder.width =", bar_builder.width)
bv.define_user_type("bar", bar_builder.immutable_copy())
print("sizeof(bar) =",bv.types["bar"].width)

Some Workarounds: If creating problematic types like these using the API the following can be a temporary workaround:

# Create a base type `foo`
foo_builder = binaryninja.StructureBuilder.create()
foo_builder.append(binaryninja.Type.int(8), "field1")
foo_builder.append(binaryninja.Type.int(8), "field2")
bv.define_user_type("foo", foo_builder.immutable_copy())

# Create a new type `bar` that inherits from `foo`
bar_builder = binaryninja.StructureBuilder.create()
bar_builder.base_structures = [ binaryninja.BaseStructure(bv.types["foo"], 0) ]
print("bar_builder.width =", bar_builder.width)
bv.define_user_type("bar", bar_builder.immutable_copy())
print("sizeof(bar) =",bv.types["bar"].width)

# Add and remove a field for `bar` to be correctly sized without declaring any fields of its own
bar_builder.append(binaryninja.Type.int(8), "field3")
bar_builder.remove(0)
bar_builder.width -= 8 # required to shrink the size otherwise padding is left
print("bar_builder.width =", bar_builder.width)
bv.define_user_type("bar", bar_builder.immutable_copy())
print("sizeof(bar) =",bv.types["bar"].width)

# Creating the type `baz` that inherits from `bar` and declares a field now works correctly
baz_builder = binaryninja.StructureBuilder.create()
baz_builder.base_structures = [ binaryninja.BaseStructure(bv.types["bar"], 0) ]
baz_builder.append(binaryninja.Type.int(8), "field3")
print("baz_builder.width =", baz_builder.width)
bv.define_user_type("baz", baz_builder.immutable_copy())
print("sizeof(baz) =",bv.types["baz"].width)

The 3 types; foo, bar and baz should now exist in the user types list with the following definitions:

struct foo
{
    int64_t field1;
    int64_t field2;
};

struct __base(foo, 0) bar
{
    __inherited int64_t foo::field1;
    __inherited int64_t foo::field2;
};

struct __base(bar, 0) baz
{
    __inherited int64_t foo::field1;
    __inherited int64_t foo::field2;
    int64_t field3;
};

WeiN76LQh avatar Oct 08 '24 01:10 WeiN76LQh