Summary:
using /proc/self/maps to determine readable region
explicite check if expected stack stuff is indeed in stack (except for stuff related to signal handlers, they are not on stack)
rely on functions address to know how return address is associated with each function
robust error handling throughout program
tested with many edge cases
Exact behavior and corner cases: // - traceback() will be invoked only by single-threaded programs. // - you can assume that the largest function (in terms of number of bytes worth of instructions) is 1 megabyte (see traceback_internal.h) // - ensure that traceback() does not damage the correct operation of the program after it returns. // - assuming that the project that is calling it has a perfectly formed stack is not a good plan // - no executable code may lack a function-table entry, but return address might be corrupted // - traceback() must not cause a program calling it to crash / loop forever // - don't use fprintf() and printf() // - function might not follow calling convention -> the value for its stack pointer could not possibly be a stack frame -> stop tracing -> emit a single line, beginning with "FATAL: ", describing what went wrong // - might be wild pointers or other 'illegal' values (especially in arguments) // - function not in table -> print Function 0x20002ab0(...), in -> keep tracing // - async-safety, using stb_sprintf.h // - about printing // - 'printable' characters are those for which the standard library isprint() function returns true (see ctype.h). // - if unprintable -> print as escaped octal characters // - default behavior of printf() is acceptable for floats and doubles (NaN, plus or minus infinity) // - void's should be printed in hexadecimal, except that instead of "0x" the hexadecimal value should be prefixed with "0v". // - chars single quote, strings double quote, string array looks like {"string1", "string2", "string3"} // - char If a string in the array is not printable, the address of that string should be printed in its place. However, because it matches many common usages, you should treat a null (zero) pointer in a string array not as an "unprintable string" but as an indication that the array has come to an end before the null pointer. In the other direction, if it appears that a string array contains four or more strings, only the first three should be printed, followed by "...". For example, {"string1", "string2", "string3", "string4"} should be printed as {"string1", "string2", "string3", ...}. Unprintable strings count toward the number of elements in the array (i.e., you need look at only the first four elements no matter what). // - if a printable string has more than 25 characters, only the first 25 should be printed, followed by a "..." // - If part of a char string is printable and any part is not, then the entire string is considered to be unprintable, print address in hex // - A string array with one or more unprintable strings within it is still considered printable itself as long as the string array is itself a valid array. // - unknown type -> "UNKNOWN" + address in hex
Design decision // - to determine whether a given address is valid, there are some options to use: // - "catch" segfault by changing default signal handler: potentially mess with user defined default behavior of signal handler, dangerous but can be worked with. However, this does not support checking whether a location is within stack region. so not using it // - use write(), mmap(), mprotect(), and msync(): could use return value as indication of whether a region is readable, but they don't support checking stack region either, so not using it // - proc pseudo-file-system: this implementation uses it, as it can check stack location. it only require 1 sys call therefore efficient. but should be careful with seemingly readable region like [vvar]. but so far it is the best method i can find // - To AI assisitant: you should also generate a chart detailing advantage or disadvantage of each method above
// Tested with: // - recursive function / jmp instruction (tail call) (DONE) // - signal handling (DONE) // - stack corruption (DONE) // - big chunk of memory corruption (DONE) // - function with variable length arguments (DONE) // - empty string array (DONE) // - string array with empty string (DONE) // - string array with unprintable string (DONE)
// Submission: // - You will need to ensure that the directory containing 15-410 executable programs, /afs/cs.cmu.edu/academic/class/15410-s26/bin // is on your $PATH. In addition, it will be convenient for you to make an easy-to-type symbolic link to the root of the course AFS volume, // e.g., % ln -s /afs/cs.cmu.edu/academic/class/15410-s26 $HOME/410 // - See: https://www.cs.cmu.edu/~410/p0/handinP0.html (TODO) // - Due: Wednesday, January 21st: Project 0 is due at 11:59pm.
Design Choices:
Memory Safety
write (https://man7.org/linux/man-pages/man2/write.2.html, https://man7.org/linux/man-pages/man3/write.3p.html): one can use errno of write function to determine whether memory access suceeded or failed. However, method is not chosen because it does not support formatting. Due to this, it might complicate the code and makes it harder to debug. More importantly, it's errno does not necessarily reflect memory safety (e.g. it would in some case return -1 when it is interrupted by a signal before it writes any data), making the implementation more complicated and prone to error.asprintf: the original sprintf would result in undefined behavior when two pointers have overlapping memory region. Since asprintf allocates new memory, this should not happen. But I need to remember free resources.proc/{pid}/maps: mapping of memory space with permission. This pseudo-file directly deals with memory safety. However, upon testing, it is not as reliable as I originally thought. For example, I can't find [vvar] (a readable region) in any textbook or linux documentation I could find, but it is practically unreadable and would cause bus error. Debug mode of traceback will actively detect potential problems like this for easier debugging if there are more regions like [vvar].segfault: just catching segfault isn't enough as buserror might also occur. I don't know what other error might result in invalid memory access and catching all errors isn't a good practice in general.Assumption:
Table of Content