I finally figured this out. The kernel does map only 2 segments. The third piece is a portion of one of the two loaded by the kernel. The run time linker, the program named in the INTERP pheader, which is /usr/lib/ld-2.24.so for me right now, changes the permissions on the mappings using mprotect()
so that there are read/write global variables, read-only global variables, and a read/execute text segment. You can see this happen using strace
, but it's easy to miss, as it's only a single mprotect()
call.
It wasn't a kernel change that caused this, it was a GNU lib C change.