Congratulations, you've found a kernel bug (in your very old Ubuntu 12.04 / Linux 3.13.0-32-generic 32-bit kernel).
mov edx, var2 passes a very large integer (an address) as the size. This is why you get garbage after the 2nd message; the write system call is reading memory up to somewhere near an unmapped page and then stopping.
On a non-buggy kernel, then write returns and execution continues until the _exit system call like you'd expect.
The instruction int 0x80 is causing the segmentation fault.
IDK whether that's more or less insane than corrupting user-space and leading to a fault later.
It's probably not worth reporting this kernel bug anywhere. Ubuntu 12.04 LTS reached End of Life in 2017. The bug doesn't exist in modern kernels and was probably either noticed or fixed by accident as part of some other change in the 7 years since that kernel was current.
What happens in non-buggy kernels with write() that eventually reads from unmapped pages
The write(2) man page definitely does not document the possibility of raising a signal on bad args, only of error codes like EFAULT.
I can't reproduce the segfault on Arch Linux with x86-64 Linux kernel 5.0.1; I get the expected garbage written and then write(2) returns the number of bytes written before it hit an unmapped page. Then execution continues until the _exit(5) system call and the process exits cleanly with status=5.
I thought write might return -EFAULT even after writing some bytes when you pass a pointer+size that includes unmapped pages, but that's not the case. The wording in the man page doesn't mention this specific case, but the wording of how other errors detected part way through writing are handled is consistent with this. (Normally those errors are from things like disk becoming full, or maybe the other side of the pipe closing.)
write(2) Linux man page
Note that a successful write() may transfer fewer than count bytes. Such partial writes can occur for various reasons; ...
...
In the event of a partial write, the caller can make another write() call to transfer the remaining bytes. The subsequent call will either transfer further bytes or may result in an error (e.g., if the disk is now full).
Linux definitely does not always transfer all the way to the end of the last mapped page when you do this. But it's interesting to see what happens for different cases.
It seems that it copies in chunks, and checks readability of each chunk as it goes. When a chunk would read from an unmapped page, the error is detected and it returns with a partial write. If you made another call with address = buf + first_retval, you'd probably get a -EFAULT. So it's very much like filling up the disk with a partial write and then detecting it by getting -ENOSPC when trying to write the rest.
Redirecting output to a file (in tmpfs) on x86-64 Linux 5.0.1 I get write() sizes of 4078. 4096-18 = 4078, and I'm using a recent ld (Binutils 2.32) so the .data section is 4k-aligned in the executable, and the start of the section is also page-aligned in memory. So the end of a page is at var2 + 4096 - len1.
$ strace ./2write > foo strace: [ Process PID=28961 runs in 32 bit mode. ] write(1, "This is message1\n\0", 18) = 18 write(1, "This is message2\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 134520850) = 4078 write(1, "This is message1\n\0", 18) = 18 write(1, "This is message2\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 134520850) = 4078 exit(5) = ? +++ exited with 5 +++
vs. writing to the terminal, I get a size of 2048
vs. writing to /dev/null, I get success with write returning 134520850. The driver for the null special block device doesn't even read the user-space memory, it just returns success from write system calls that make it that far. So nothing ever checks for -EFAULT.
Piping the output to wc, I got a surprising 18-byte partial-write on the first bad call, and -EFAULT on the next.
strace ./2write | wc execve("./2write", ["./2write"], 0x7ffdba771cf0 /* 53 vars */) = 0 strace: [ Process PID=29008 runs in 32 bit mode. ] write(1, "This is message1\n\0", 18) = 18 write(1, "This is message2\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 134520850) = 18 write(1, "This is message1\n\0", 18) = 18 write(1, "This is message2\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 134520850) = -1 EFAULT (Bad address) exit(5) = ? +++ exited with 5 +++ 3 9 54
On subsequent runs of the program, I got -EFAULT right away. I'm guessing that Linux may have allocated more memory for a pipe buffer after the first call, so then it was able to look far enough ahead to notice the bad address right away, before copying any data.
peter@volta:/tmp$ strace ./2write | wc execve("./2write", ["./2write"], 0x7fff868a41b0 /* 53 vars */) = 0 strace: [ Process PID=29015 runs in 32 bit mode. ] write(1, "This is message1\n\0", 18) = 18 write(1, "This is message2\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 134520850) = -1 EFAULT (Bad address) write(1, "This is message1\n\0", 18) = 18 write(1, "This is message2\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 134520850) = -1 EFAULT (Bad address) exit(5) = ? 2 6 36
mov edx, var2as the length. Pointers are large integers. The system call should eventually just return-EFAULTthough, once you get to an unmapped page after copying some readable data to the given file descriptor. (Actually they return2048, not an error, when they stop from the unmapped page, on Linux 5.0.1). And then it should return. EAX=1 / int 0x80 issys_exitgcc -no-pie -nostdlib -m32orld -melf_i386. Use a debugger to single-step. Maybe also trystraceto see what happens to the_exitsystem call.mov edx, len2 ```` instead of ```` mov edx, var2thank you @PeterCordes