 Hi everybody, welcome to Delhi stage. And I think we can start up with our first session for Python India 2020. So the first session we have today is understanding peak memory layout with PernDebat. I hope I pronounced it correctly, Rohit. And our speaker for today is Rohit Keshri. I think Rohit, you'll be up. Yes, hi, how are you? Yeah, I'm good. Thank you. Okay, thank you so much. Glad to have you at Pycon India. And so for everybody who's tuned in on Hopin, so we will have a 25 minute session by Rohit on peak memory and followed by five minutes of questions. So please do put up your questions on the Hopin platform and we will take that up at the end of the talk, right? Does that sound good, Rohit? Yep. Okay, so I think with that you're on. All the best. Thank you. Hello, everyone. Thank you for joining me. My name is Rohit Keshri. I work in Red Hat as Product Security Engineer in Kernel Space. I'm going to talk about dynamic memory allocation mechanism in user space programming. In this, we will be talking about heap internals. We'll know how the heap formation takes place, its metadata, register contents, and so many things about this. To understand the user space memory layout, we will be using a tool known as PernDebug. This is a Python-based module and is running as a plugin with GDB. This is an open source project and here is the GitHub page for this. PernDebug tool adds some interesting introspection mechanism to understand the resources, especially the memory layouts. And here is, in the case of heap, it is also very much helpful. In the Hall segment of this page, you will see the installation guidelines. It's very simple. You just need to clone up a script and you just need to run it. It even installs GDB for you on your system and that is ready to go with this tool. The agenda for today is like, we will be talking about the memory layout in C. We will know about heap internals. We will know what are arena's heap chunks. We will also see a live demo on the tool itself and we will try to use a C program, which is small and where we will be in a process of heap formation and we'll learn so many things. So when a user space program gets started, it gets some default resources allocated and memory is one of them. Looking closely at this memory, you will see there are so many different types of memories that are getting associated to the program. Stack is one of them, which is used to store the local variables and the passing arguments of the function and also it holds the returns. Any global variables defined in the program are stored in the DS data segment, which is if initialized. If the global variables are not initialized, they are the DSS segment because they are made default to zero by design. Text segment contains the executable instructions, which is a read-only memory. So the above four memory are the default memory that a program gets as a default by the kernel, but there is also one more memory called heap segment, which is not a default thing. And it is only there when the programmer demands it with the help of certain functions. To understand this memory layout further, I have a small program where I have defined some variables for you to see where it goes and gets stored. Here int x, which is a global variable because this is defined outside the main is initialized and that's why this is going to be stored in the data segment. Int A and float B are two local variables and they will be stored in stack. There is another global variable, static y, which is not initialized and this goes to the BSS segment. PTR is a local variable, which stores in the stack, but this is a special type of variable which is designed to store addresses. And this is going to get an address from a function called malloc. Malloc by design is a function which is defined to reserve memory from the heap. And on success, malloc returns you the address of the heap that is reserved. Every time a programmer allocates a heap, it is his responsibility to free this after its usage and that is done by the function called free. If this is not done, this becomes a memory leak and this can affect your system in long go. So heap is not a default allocation and this only comes in the runtime. It is managed by functions like malloc and when you want to free this, you can call free. Apart from that, there are colloc and realloc, but they are just internally calling malloc and free. So this is the ML commission of malloc and free. Heap is a memory which grows from lower memory address space to higher memory address space, which is just the opposite of the stack. So malloc is a library function provided by the G-Lipsy and this is a function which is collection of so many different underlying functions and the metadata where metadata consists of arena, heap and chunks. To understand the prototype of malloc, it is very simple. It takes only one argument as the size. The size says how much memory you want from the heap reservation and on success, it returns you the heap address that gets reserved. Adenas are the internal building blocks of the heap which actually administers the complete heap memory region. It looks after the free, it look after the any usage of the heap. Heap in turn is a contiguous block of memory which gets broken down into pieces whenever there is a request from the malloc. That means chunks are the fundamental unit of the malloc that malloc deals with internally. So every time a malloc is requested, there is a chunk gets created. In this first diagram, you will see there is a unused pool of memory which is the heap and this is also known as top chunk. Later, there is a first malloc request of 16 bytes which goes to the top chunk and asks for a smaller amount of memory which gets assigned to the user in the form of chunk A. Later, there is a second malloc request of again 16 bytes which again gets addressed by the top chunk and a new chunk comes into picture that is chunk B. So every time a malloc request is raised, top chunk gets divided into smaller fragments and that gets associated to the user. So top chunk is a free pool of heap memory. To after knowing the internals of the heap, let's directly jump to the demonstration with the help of this tool. I have a small program and we'll see how the heap formation takes place. I have a small executable for you. Also, the check set is a tool by which you can see the security stands of a binary. It tells you with which mitigations techniques this binary is built. These are the flags with which the library is built. Here you can see that this binary is built with full railroad, means there are some parts which are made read-only so that attacker cannot violate and rewrite that and produce some attack on the system. There is also, it is enabled with NX enabled, but this is not enabled with Kennedy and also the position independence address space is also not enabled on this binary. I'm running this binary with GDB. You will notice since we have started the GDB, we see that there is a terminal that is represented as Power and Debug. This is because Power and Debug is working as a plugin which runs with the GDB. I'm setting a breakpoint at main so that we will see the code line by line and we'll understand this better. Then we will just run the program. Welcome to the tool. You can see there are so many things going on on the screen. We'll see them individually. The first thing you will see there is the register related informations on the screen. Then you can see the disassembly part of this code, source code. Then there is a source itself. Then you will see stack part and at the end you see the back traces. I'm making this view little smaller so that this is easily understood. So I'm setting the context. I'm setting the context to view only the code. And now you can see that there is only code viewed. We can call those windows by the command like, so if you want to see the register contents, you can just call registers. If you want to see the stack, you can just call the stack. So I'm just limiting this view to the source code right now. Since we have set a break point at main, now our execution is halting at the start of the main. I want to see what is the default allocations that this program has got. And I am focusing on the memory part. So there is a command called VMMAP. This gives you the details of all the memory stances of this program. You will notice that there is a different color conventions shown here. Here a stack is represented in yellow, heap in blue, code that is the text section in red, and data in violet. You will notice there is no heap which should be represented with blue. And this is right. This is because the heap is not a default memory and this is only coming into picture when the programmer demands. So my first instruction in this main is calling a heap by function malloc. And we are requesting a nine bytes of heap. So I'm just running next or end. So you can see the line number five has been executed and the cursor is waiting at seven. So by now I think there should be some heap. Let's see. Yes, you can see the heap which is represented in blue is not there. You can also notice that this is starting at the address 0x602000, which is the start address of this heap. One more interesting data here is this column which talks about the total size of this heap and this is in hex. So this is 0x21000 which is approximately 135,000 bytes which is quite huge because we had only requested for nine bytes. Let's see what is there inside the heap and this can be done by this command. This command actually shows you all the details of the heap and here you can see again the different coloring conventions. Here you see that there is something of the color sign which is 32 bytes because eight bytes and four of such sets. So this is the first allocation that was requested and this is the first chunk. This is the chunk that was requested with the first malloc of size nine bytes. The first thing you will notice is that this request is more than nine bytes because eight bytes of four such sets means 32 bytes. I want to see what is the address that was written by the malloc and that should be stored in A. So we can do that by print A. Notice the address stored in this pointer is 602010 which is the second eight byte of this chunk. So you will notice that this is the place from where the user space data actually starts for the programmer usage and the first eight byte is something that is reserved and this is reserved for kernel and that is the meta data. It holds the meta data of the chunk. So you will notice that every chunk later in this program will have a different meta data individually for this chunks. So the user data that we have got now is of 24 bytes on the request of first nine bytes. So what I'll do is I'll just run the next command and we'll see what is the reservation of heap when we requested for a one byte of memory. In the viz you will see there is a second chunk now coming in the list. This is again of 32 bytes and again the user data starts from the second eight byte of this chunk and the first eight byte is again the meta data field. Let's see what happens when we request for a malloc of zero bytes. Technically it shouldn't be allocated a memory, right? Let's see what happens. Notice there is a third chunk in shown in green which is again of 32 bytes where eight bytes of meta data and 24 bytes of user data. So things boils down to like the default chunk that a malloc can return is of 32 bytes and it cannot be smaller than this. First thing, second thing you will notice that the user data starts from the next eight byte and the first eight byte is reserved for the chunks meta data. You can notice that every chunk has this meta data reserved and only the user data starts from the next eight byte. So since we have 24 bytes of user data given to every request and that is of size 24, let's see what happens when we demand a malloc of size 24 bytes. There is a fourth chunk and this is again a size of 24 bytes which was right because 24 bytes of demand should be suffice in this default chunk which is of 32 bytes when eight bytes of meta data and 24 bytes of user data. What happens when we ask for a memory more than 24 bytes? That is let's say 25 bytes. Notice the chunk is no bigger and this time you have allocated 48 bytes where eight bytes of meta data and then 40 bytes of user space data. Now I'll talk about what is the first eight bytes of this meta data. The first eight byte of every chunk represents the size field of this chunk. We have seen that the default chunk size that we have got previously in the four sequences were of 32 bytes. In hex 32 means 0x20. So the first chunk, second, third and fourth are all of equi-sized that is the default size and its size is 20, 0x20. Later, since the fifth chunk that we had requested was not able to fulfill the request, so it got a bigger chunk and this time the chunk size was increased to 48 bytes. That means the next after default the chunk size increases by 16 bytes and that's why you can see the extra 16 added to this chunk size where 48 in hex is represented as 0x30. But you will notice in all the meta data there is one bit extra added to the size field, why? This is because the chunk size meta data does not only hold the meta data for the size but its last nibble also represents the flags. These flags are some special flags and in this case there is a flag known as prev in use flag which is set for all the chunks. What is this prev in use flag doing? Actually the prev in use flag is used to show what is the status of the adjacent chunk. So if the adjacent chunk is reserved, this flag is set and if that is free, this is reset. So we can see that we have just reserved these five chunks and that's why its adjacent chunk is reserved and that's why this flag is set. But you will ask why the first chunks prev in use flag is set. So theoretically the first chunks size field will have the prev in use flag as set by default. That is by design. You will also ask why the first aid byte was not used and we have seen that this is the starting of the heap. So why designed? Colonel understands that heap has started indeed from the 0x20 to 000. But for the user, the first user access address starts 16 bytes later to the heap start. And that's why you would have noticed the first chunks user space data started from this which we also saw by print a. Now you would also feel what is the last byte doing here and what is this seen in every of the chunk reservations? Like you could see this in all the reservations it gets concatenated at the end. This was even in the first chunk. So like every chunk has a size field to represent the chunk size, the top chunk that we had discussed before top chunk is a free source of heap. Any request comes to top chunk and they demand a free memory from the top chunk and top chunk gets a slice and reserves that for the demand. Similarly, there is a top chunk size field. This is confusing, but you'll have to understand that every chunk like had a size field top chunk also has a size field. And every time a malloc request is raised. So since the first argument of this malloc is the size that gets compared to the top chunk size. If the top chunk size has a larger value that means there is free reserve pull and that's why it can address the forthcoming request of malloc. And so you can see that this field that is represented as the last eight byte is the top chunk size field. This was also seen in the VM map command that I showed you earlier that the heap reserved was 0x21000 and this was approximately of the size of 135,000 bytes. So there is a reserve pull, there is a request. Request gets satisfied by the top chunks and the top chunks size field get it decremented as the allocations is done. In many versions of GLFC, this top chunk size field is not subjected to an integrity check. So consider if there is a reservation of memory like malloc in this case, example is I have reserved a four bytes of memory from the malloc and I have written four bytes of data in this memory. So the first diagram can be seen that these reservations of memory is seen like this after we have reserved it and we have written some data. In the second diagram, you will see that there is the same four bytes of memory reserved but we have written 100 bytes of data. So internally it corrupts the memory which is not even reserved to this address space and this can even flood the top chunks size field because GLFC did not have the integrity check before the GLFC version 2.2.2.9. Later versions this integrity check was added. So imagine if this metadata gets corrupted, the forthcoming request from the malloc and if there is no top chunk heap is empty but due to corruption, if this field is corrupted to a very large value, kernel will think that there is still a huge top chunk already available and it will keep on giving the next forthcoming malloc request and in this way, this can corrupt other user spaces, programs, memory and which is generally silent and when those programs run, this can encounter a problem and there could be segmentation faults. This problem was seen and this was known as house of force. Actually there are similarly different types of problems recorded earlier and this has been given some names like house of force, house of prime, mind, law and spirit. So these are on the basis of different metadata corruption and one we are discussing here is house of force which gets seen when there is a corruption in the top chunks size field. Now, since we have talked about reservations, it's time to talk about free. I have a second program. I'm setting the break point and setting the context only to the code so that we do not have so many things on the screen. You can see there is a similar reservation of heap. I'm just running these first three mallocs. So now we must have got some chunks. Yes, so we have got three chunks. Now I want to free this. There are several lists of free sources where every time you free the chunks that gets listed to some of the free list. I'm going to talk here about a list known as fastbin which can be seen by command fastbin. Here you will see a list of such pools and these pools are defined on the basis of chunk size. You notice the default chunk size was 0x20. Second time when the chunk size increased by 16 bytes it's became 0x30. And similarly, the chunk size increases by 16 and these are the list of forthcoming chunk sizes. In this we know that the three reservations that we have done is of A, B and C are all falling in the category of 0x20 chunk size. Let's free the first malloc that is chunk A. Let's see what has happened to the fastbin. You will notice the first list which was of size 0x20 has got one entry which was before empty. This is because the first chunk of this size has been freed and that gets attached to the head of this list. Let's see what happened to the list. You will notice the first chunk which was reserved before is now represented in the fastbin and that is also of the size 0x20 and this is at the index 0. Let's free the second chunk. Let's see the fastbin. You will notice the second entry is there added to the head. If you are curious to see what were there in A and B, I'll just print this for you. You will notice the A which was first released is here. But the A that was having was some different address which was 16 byte ahead. This is because the user only know about the user data and kernel knows its heap from metadata onwards. That's why there is a difference in the address which is 16 byte difference. But altogether they both are referring to the same chunk. Let's do the third free and now fastbin shows the third entry also in the 0x20 list. Let's see what, yeah. Sorry to interrupt but we are running short of time and we have a question that would like to take. Yes, sure. Are you doing that? Just I can take two minutes. Okay, sure. So the question is, yeah. I believe the metadata is a function of allocator that is being used, right? Question mark. So this is a good way to understand how do allocators allocate stuff. What's up? All right, yeah. Functions are the, there are actually some inline functions that are doing the reservations for Miloc and this is actually done by the kernel and that completely depends upon the size that Miloc is requested with. There makes some indexes according to the size and then they decide what should be the chunk that is to be allocated. I hope I answered but I think if the questions are there in the list, I'll definitely see that again and answer that if you have misunderstood. Sure, so for those of you who is listening on popping, please feel free to join Zulip Chat and we have a stream just for deli stage and Rohit will be present on the deli stage to take all of your questions. So Rohit please also head to the deli stage stream on Zulip. Thanks a lot, that's wonderful talk. Thank you. Sure, so just the two minutes can I take on this recommendation? Yeah. So you can see here that the last free is done and this is also listed in the fast wins head. Now what happens when there is a further allocation of the memory with the help of Milok? Obviously if the chunk size would be same, it will first go to the fast win. Let's say in this line number 14 is actually demanding for a memory of Milok of one, which is obviously going to get a default chunk of 32 bytes. Let's do that. So now you can see the fast win is having only the two entries because the third entry which was there in the head is now allocated to D. We can see that by D. Similarly, the consequence calls like next and again I'm doing the next. Now you will see that the fast win is empty. This is because the further request that comes first comes into the fast win. And if there is some chunk that was recently freed that gets associated to the new request. And in this way the re-breaking of the chunk that is stop chunk does not happen. And this actually saves with so many fragmentation of memory. So one thing more I just missed. You can see that now all the chunks that was actually represented in the fast win is now omitted and they all are the free chunks which got associated with the request. So I have also covered that on the slides and yep. So there are several free lists in the Melloc terminology but we have only talked about fast wins. Fast wins are smaller chunks request which when freed goes to fast wins. If the size of the freeing chunk are in the comparison of the top chunk size that goes directly to the top chunk. There are something also called unsorted wins which further divides into small wins and large wins. And there is another a T cache which is which we are not discussing in this talk. So this was all with this presentation. Thank you Roy. Thanks a lot. The attendees will be present in the Delhi stage three on July. So feel free to join and have a conversation. Thanks a lot. Thank you. Thank you everyone. Thank you for joining.