Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

set memory to undefined in allocator implementations rather than interface #4298

Open
andrewrk opened this issue Jan 27, 2020 · 3 comments
Open
Labels
accepted This proposal is planned. contributor friendly This issue is limited in scope and/or knowledge of Zig internals. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. standard library This issue involves writing Zig code for the standard library.
Milestone

Comments

@andrewrk
Copy link
Member

Follow-up from #4087, where I wrote:

Yes it makes sense to do it in the interface rather than in allocator implementations.

Having looked at the patch and seen this comment:

         // Note: can't set shrunk memory to undefined as memory shouldn't be modified on realloc failure 

I am now convinced that it should be done in the allocator implementations rather than the interface, where this problem can be solved. My reasoning is that a robust debug allocator will want to do this (handle realloc correctly with regards to defined), and while it wouldn't hurt semantically to have the memory set to undefined twice, this is an area where performance might matter a great deal, and it is better design to have one canonical place where the memory is set to undefined.

The allocator implementations may be able to much more efficiently set the data to undefined for allocations as well.

One concern is that allocator implementations will "forget" to do this. For this I propose that the standard library actually include a robust allocator test, which will work on any std.mem.Allocator API. People writing their own allocators should be able to call std.testing.allocatorInterfaceStressTest(&my_allocator, options); and be reasonably confident their allocator is working correctly, including with regards to setting memory to undefined. The options could include a flag to disable testing whether the memory gets set to undefined in debug/release-safe builds.

@andrewrk andrewrk added standard library This issue involves writing Zig code for the standard library. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. labels Jan 27, 2020
@andrewrk andrewrk added this to the 0.7.0 milestone Jan 27, 2020
@andrewrk andrewrk added the accepted This proposal is planned. label Mar 5, 2020
@andrewrk andrewrk modified the milestones: 0.7.0, 0.8.0 Oct 17, 2020
@andrewrk andrewrk modified the milestones: 0.8.0, 0.9.0 May 19, 2021
@andrewrk andrewrk modified the milestones: 0.9.0, 0.10.0 Nov 20, 2021
@andrewrk andrewrk modified the milestones: 0.10.0, 0.11.0 Apr 16, 2022
@andrewrk andrewrk modified the milestones: 0.11.0, 0.12.0 Apr 9, 2023
@andrewrk andrewrk modified the milestones: 0.13.0, 0.12.0 Jun 29, 2023
@dimdin
Copy link

dimdin commented Apr 28, 2024

There is another reason:
Setting bytes to undefined in free, hides double free detection.
The second free becomes use after free, when @memset to undefined happens.

@Cloudef
Copy link
Contributor

Cloudef commented May 4, 2024

I feel like this can easily lead to multiple useless memsets when one wraps multiple allocators on top of each other. There possibility should also be a way to allocate without memset for use-cases where you want to reserve big chunk of memory but not touch it

@gabe-lee
Copy link

gabe-lee commented Jun 21, 2024

So the tl;dr as I see it is:
CASE A: Allocator interface handles @memset(_, undefined)

  • Difficulties undefining in certain cases because interface has limited access to underlying implementation
  • Memory undefining performed only once and guaranteed by the interface (or is expected to be)

CASE B: Allocator internal implementation handles @memset(_, undefined)

  • When using allocators that themselves take child/backing allocators, each level of allocator will run redundant @memset() on the same memory region
  • Allocator implementation author is responsible for ensuring memory undefining, possible to forget

A third option (CASE C) I propose (that would require a breaking change to how some allocators are initialized) would be to have ALL allocators required to be built from a comptime factory function (like GeneralPurposeAllocator), and take a config struct with an option like set_undefined_on_free that tells the implementation whether they should be responsible for their own memory undefining or not.

The downside to this is that instead of the allocator AUTHOR being solely responsible for memory undefining, the allocator USER would also be responsible for making sure they pass the right setting to each level of allocator so that all memory is set to undefined at least once (idealy exactly once). This problem can be mitigated by giving the setting a default value of set_undefined_on_free: bool = true so that by default memory is always undefined, possibly multiple times.

Example code might look something like this:

// PageAllocNoUndef is just the std.heap.page_allocator that doesnt undefine memory on free
const PageAllocNoUndef: type = std.heap.PageAllocator(.{.set_undefined_on_free = false});
// MyAllocatorUndef will hande all undefining of memory
const MyAllocUndef: type = MyAllocator(.{.set_undefined_on_free = true});
var base_alloc: Allocator = PageAllocNoUndef.allocator();
var my_allocator: MyAllocUndef = MyAllocUndef.init(base_alloc);

Or alternatively

// PageAllocUndef is just the std.heap.page_allocator that DOES undefine memory on free
const PageAllocUndef: type = std.heap.PageAllocator(.{});
// MyAllocatorNoUndef will assume base allocator handles undefining
const MyAllocNoUndef: type = MyAllocator(.{.set_undefined_on_free = false});
var base_alloc: Allocator = PageAllocUndef.allocator();
var my_allocator: MyAllocNoUndef = MyAllocNoUndef.init(base_alloc);

If user doesn't know what to set, defaults ensure memory always undefined, albeit multiple times

const PageAlloc: type = std.heap.PageAllocator(.{});
const MyAlloc: type = MyAllocator(.{});
var base_alloc: Allocator = PageAlloc.allocator();
// Freed memory will be `@memset(_, undefined)` twice
var my_allocator: MyAlloc = MyAlloc.init(base_alloc);

If user explicitly sets all layers to not undefine, it could prevent detection of illegal behavior in Debug mode, but could ALSO be a 'feature', if the user (for some reason) wanted to manually set memory undefined somewhere else external to the allocator itself.

const PageAllocNoUndef: type = std.heap.PageAllocator(.{.set_undefined_on_free = false});
const MyAllocNoUndef: type = MyAllocator(.{.set_undefined_on_free = false});
var base_alloc: Allocator = PageAllocNoUndef.allocator();
// Freed memory will never be `@memset(_, undefined)`
var my_allocator: MyAlloc = MyAllocNoUndef.init(base_alloc);
// Do some stuff with memory
// Eventually free all memory and deinit allocators
setUsedMemoryAsUndefined(mem: []u8);

@andrewrk andrewrk added the contributor friendly This issue is limited in scope and/or knowledge of Zig internals. label Jul 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted This proposal is planned. contributor friendly This issue is limited in scope and/or knowledge of Zig internals. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. standard library This issue involves writing Zig code for the standard library.
Projects
None yet
Development

No branches or pull requests

4 participants