authorgravatar for matthew.h.borkowski@gmail.comMatthew Borkowski <matthew.h.borkowski@gmail.com> 2021-10-27 19:51:05-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-10-29 19:22:24-04:00
log5ff01bd820ea08005a422f046ad5bbad663b0dab
tree0ccf582788e6fee6fb8c8118c33e5ea49b737fea
parent544d7d99822912a4f85768b0df882775e5fdb80d

gpa: fix memory limit accounting for large allocations


1 files changed, 59 insertions(+), 40 deletions(-)

lib/std/heap/general_purpose_allocator.zig+59-40
...@@ -210,6 +210,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -210,6 +210,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
210210
211 const LargeAlloc = struct {211 const LargeAlloc = struct {
212 bytes: []u8,212 bytes: []u8,
213 requested_size: if (config.enable_memory_limit) usize else void,
213 stack_addresses: [trace_n][stack_n]usize,214 stack_addresses: [trace_n][stack_n]usize,
214 freed: if (config.retain_metadata) bool else void,215 freed: if (config.retain_metadata) bool else void,
215 ptr_align: if (config.never_unmap and config.retain_metadata) u29 else void,216 ptr_align: if (config.never_unmap and config.retain_metadata) u29 else void,
...@@ -528,13 +529,9 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -528,13 +529,9 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
528 if (config.retain_metadata and entry.value_ptr.freed) {529 if (config.retain_metadata and entry.value_ptr.freed) {
529 if (config.safety) {530 if (config.safety) {
530 reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));531 reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free));
531 if (new_size == 0) {532 // Recoverable if this is a free.
532 // Recoverable. Restore self.total_requested_bytes if needed.533 if (new_size == 0)
533 if (config.enable_memory_limit) {
534 self.total_requested_bytes += old_mem.len;
535 }
536 return @as(usize, 0);534 return @as(usize, 0);
537 }
538 @panic("Unrecoverable double free");535 @panic("Unrecoverable double free");
539 } else {536 } else {
540 unreachable;537 unreachable;
...@@ -556,7 +553,29 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -556,7 +553,29 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
556 });553 });
557 }554 }
558555
559 const result_len = if (config.never_unmap and new_size == 0) 0 else try self.backing_allocator.resizeFn(self.backing_allocator, old_mem, old_align, new_size, len_align, ret_addr);556 // Do memory limit accounting with requested sizes rather than what backing_allocator returns
557 // because if we want to return error.OutOfMemory, we have to leave allocation untouched, and
558 // that is impossible to guarantee after calling backing_allocator.resizeFn.
559 const prev_req_bytes = self.total_requested_bytes;
560 if (config.enable_memory_limit) {
561 const new_req_bytes = prev_req_bytes + new_size - entry.value_ptr.requested_size;
562 if (new_req_bytes > prev_req_bytes and new_req_bytes > self.requested_memory_limit) {
563 return error.OutOfMemory;
564 }
565 self.total_requested_bytes = new_req_bytes;
566 }
567 errdefer if (config.enable_memory_limit) {
568 self.total_requested_bytes = prev_req_bytes;
569 };
570
571 const result_len = if (config.never_unmap and new_size == 0)
572 0
573 else
574 try self.backing_allocator.resizeFn(self.backing_allocator, old_mem, old_align, new_size, len_align, ret_addr);
575
576 if (config.enable_memory_limit) {
577 entry.value_ptr.requested_size = new_size;
578 }
560579
561 if (result_len == 0) {580 if (result_len == 0) {
562 if (config.verbose_log) {581 if (config.verbose_log) {
...@@ -599,18 +618,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -599,18 +618,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
599 const held = self.mutex.acquire();618 const held = self.mutex.acquire();
600 defer held.release();619 defer held.release();
601620
602 const prev_req_bytes = self.total_requested_bytes;
603 if (config.enable_memory_limit) {
604 const new_req_bytes = prev_req_bytes + new_size - old_mem.len;
605 if (new_req_bytes > prev_req_bytes and new_req_bytes > self.requested_memory_limit) {
606 return error.OutOfMemory;
607 }
608 self.total_requested_bytes = new_req_bytes;
609 }
610 errdefer if (config.enable_memory_limit) {
611 self.total_requested_bytes = prev_req_bytes;
612 };
613
614 assert(old_mem.len != 0);621 assert(old_mem.len != 0);
615622
616 const aligned_size = math.max(old_mem.len, old_align);623 const aligned_size = math.max(old_mem.len, old_align);
...@@ -651,19 +658,28 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -651,19 +658,28 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
651 if (!is_used) {658 if (!is_used) {
652 if (config.safety) {659 if (config.safety) {
653 reportDoubleFree(ret_addr, bucketStackTrace(bucket, size_class, slot_index, .alloc), bucketStackTrace(bucket, size_class, slot_index, .free));660 reportDoubleFree(ret_addr, bucketStackTrace(bucket, size_class, slot_index, .alloc), bucketStackTrace(bucket, size_class, slot_index, .free));
654 if (new_size == 0) {661 // Recoverable if this is a free.
655 // Recoverable. Restore self.total_requested_bytes if needed, as we662 if (new_size == 0)
656 // don't return an error value so the errdefer above does not run.
657 if (config.enable_memory_limit) {
658 self.total_requested_bytes = prev_req_bytes;
659 }
660 return @as(usize, 0);663 return @as(usize, 0);
661 }
662 @panic("Unrecoverable double free");664 @panic("Unrecoverable double free");
663 } else {665 } else {
664 unreachable;666 unreachable;
665 }667 }
666 }668 }
669
670 // Definitely an in-use small alloc now.
671 const prev_req_bytes = self.total_requested_bytes;
672 if (config.enable_memory_limit) {
673 const new_req_bytes = prev_req_bytes + new_size - old_mem.len;
674 if (new_req_bytes > prev_req_bytes and new_req_bytes > self.requested_memory_limit) {
675 return error.OutOfMemory;
676 }
677 self.total_requested_bytes = new_req_bytes;
678 }
679 errdefer if (config.enable_memory_limit) {
680 self.total_requested_bytes = prev_req_bytes;
681 };
682
667 if (new_size == 0) {683 if (new_size == 0) {
668 // Capture stack trace to be the "first free", in case a double free happens.684 // Capture stack trace to be the "first free", in case a double free happens.
669 bucket.captureStackTrace(ret_addr, size_class, slot_index, .free);685 bucket.captureStackTrace(ret_addr, size_class, slot_index, .free);
...@@ -745,21 +761,15 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -745,21 +761,15 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
745 const held = self.mutex.acquire();761 const held = self.mutex.acquire();
746 defer held.release();762 defer held.release();
747763
764 if (!self.isAllocationAllowed(len)) {
765 return error.OutOfMemory;
766 }
767
748 const new_aligned_size = math.max(len, ptr_align);768 const new_aligned_size = math.max(len, ptr_align);
749 if (new_aligned_size > largest_bucket_object_size) {769 if (new_aligned_size > largest_bucket_object_size) {
750 try self.large_allocations.ensureUnusedCapacity(self.backing_allocator, 1);770 try self.large_allocations.ensureUnusedCapacity(self.backing_allocator, 1);
751
752 const slice = try self.backing_allocator.allocFn(self.backing_allocator, len, ptr_align, len_align, ret_addr);771 const slice = try self.backing_allocator.allocFn(self.backing_allocator, len, ptr_align, len_align, ret_addr);
753772
754 // The backing allocator may return a memory block bigger than
755 // `len`, use the effective size for bookkeeping purposes
756 if (!self.isAllocationAllowed(slice.len)) {
757 // Free the block so no memory is leaked
758 const new_len = try self.backing_allocator.resizeFn(self.backing_allocator, slice, ptr_align, 0, 0, ret_addr);
759 assert(new_len == 0);
760 return error.OutOfMemory;
761 }
762
763 const gop = self.large_allocations.getOrPutAssumeCapacity(@ptrToInt(slice.ptr));773 const gop = self.large_allocations.getOrPutAssumeCapacity(@ptrToInt(slice.ptr));
764 if (config.retain_metadata and !config.never_unmap) {774 if (config.retain_metadata and !config.never_unmap) {
765 // Backing allocator may be reusing memory that we're retaining metadata for775 // Backing allocator may be reusing memory that we're retaining metadata for
...@@ -768,6 +778,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -768,6 +778,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
768 assert(!gop.found_existing); // This would mean the kernel double-mapped pages.778 assert(!gop.found_existing); // This would mean the kernel double-mapped pages.
769 }779 }
770 gop.value_ptr.bytes = slice;780 gop.value_ptr.bytes = slice;
781 if (config.enable_memory_limit)
782 gop.value_ptr.requested_size = len;
771 gop.value_ptr.captureStackTrace(ret_addr, .alloc);783 gop.value_ptr.captureStackTrace(ret_addr, .alloc);
772 if (config.retain_metadata) {784 if (config.retain_metadata) {
773 gop.value_ptr.freed = false;785 gop.value_ptr.freed = false;
...@@ -782,10 +794,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -782,10 +794,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
782 return slice;794 return slice;
783 }795 }
784796
785 if (!self.isAllocationAllowed(len)) {
786 return error.OutOfMemory;
787 }
788
789 const new_size_class = math.ceilPowerOfTwoAssert(usize, new_aligned_size);797 const new_size_class = math.ceilPowerOfTwoAssert(usize, new_aligned_size);
790 const ptr = try self.allocSlot(new_size_class, ret_addr);798 const ptr = try self.allocSlot(new_size_class, ret_addr);
791 if (config.verbose_log) {799 if (config.verbose_log) {
...@@ -1183,3 +1191,14 @@ test "double frees" {...@@ -1183,3 +1191,14 @@ test "double frees" {
1183 try std.testing.expect(gpa.large_allocations.contains(@ptrToInt(normal_large.ptr)));1191 try std.testing.expect(gpa.large_allocations.contains(@ptrToInt(normal_large.ptr)));
1184 try std.testing.expect(!gpa.large_allocations.contains(@ptrToInt(large.ptr)));1192 try std.testing.expect(!gpa.large_allocations.contains(@ptrToInt(large.ptr)));
1185}1193}
1194
1195test "bug 9995 fix, large allocs count requested size not backing size" {
1196 // with AtLeast, buffer likely to be larger than requested, especially when shrinking
1197 var gpa = GeneralPurposeAllocator(.{ .enable_memory_limit = true }){};
1198 var buf = try gpa.allocator.allocAdvanced(u8, 1, page_size + 1, .at_least);
1199 try std.testing.expect(gpa.total_requested_bytes == page_size + 1);
1200 buf = try gpa.allocator.reallocAtLeast(buf, 1);
1201 try std.testing.expect(gpa.total_requested_bytes == 1);
1202 buf = try gpa.allocator.reallocAtLeast(buf, 2);
1203 try std.testing.expect(gpa.total_requested_bytes == 2);
1204}