I'm writing a hello-world program in i386 assembly for OpenBSD 7.8. The program works if the string (buf argument of write(2)) is in .data or on the stack, but write(2) returns EFAULT (== 14) if the argument is in .rodata. What am I doing wrong? Is there some protection mechanism which I'm not configuring correctly?
Please note that these programs are not position-independent executables (because they are ET_EXEC rather than ET_DYN).
Short program with string in .rodata, write(2) returns EFAULT on OpenBSD 7.3, 7.4, 7.7 and 7.8; it succeeds on OpenBSD 6.0, 7.0 and 7.2:
; Compile with: nasm -O0 -o w1 w1.nasm && chmod +x w1
cpu 386
bits 32
org 0x08048000
Ehdr_OSABI equ 0 ; SYSV.
PT:
.LOAD equ 1
.NOTE equ 4
.OPENBSD_SYSCALLS equ 0x65a3dbe9
Elf32_Ehdr: ; ELF-32 i386 header.
db 0x7f, 'ELF', 1, 1, 1, Ehdr_OSABI
db 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0
dd 1, _start, Elf32_Phdr0-$$, 0, 0
dw Elf32_Phdr0-$$, 0x20, (Elf32_Phdr_end-Elf32_Phdr0)>>5
dw 0x28, 0, 0
Elf32_Phdr0: dd PT.LOAD, 0, $$, $$, text_end-$$, text_end-$$, 5, 0x1000
Elf32_Phdr1: dd PT.LOAD, text_end-$$, text_end+0x1000, text_end+0x1000
dd file_end-text_end, mem_end-text_end, 6, 0x1000
Elf32_Phdr2: dd PT.NOTE, Elf32_Note-$$, Elf32_Note, Elf32_Note,
dd Elf32_Note_end-Elf32_Note, Elf32_Note_end-Elf32_Note, 4, 4
Elf32_Phdr3: dd PT.OPENBSD_SYSCALLS, Elf32_osc-$$, 0, 0
dd Elf32_osc_end-Elf32_osc, Elf32_osc_end-Elf32_osc, 4, 4
Elf32_Phdr_end:
Elf32_Note: ; Same PT_NOTE as in /bin/cat in Minix 3.3.0.
dd 8, 4, 1 ; Size of the name, size of the value, node type.
db 'OpenBSD', 0 ; 8-byte name.
dd 0 ; Version number.
Elf32_Note_end:
Elf32_osc: dd syscall_location1, 4, syscall_location2, 1
Elf32_osc_end:
_start:
push byte msg_hello.end-msg_hello
push dword msg_hello
push byte 1 ; STDOUT_FILENO
push eax ; Dummy return adddress.
push byte 4 ; SYS.write.
pop eax
syscall_location1: equ $
int 0x80 ; OpenBSD i386 syscall.
jc short .exit
xor eax, eax ; EXIT_SUCCESS.
.exit:
push eax ; Dummy return address.
push eax ; Exit code will be errno from the write(2) above.
push byte 1 ; SYS.exit.
pop eax
syscall_location2: equ $
int 0x80 ; OpenBSD i386 syscall.
msg_hello: ; We put it to .rodata, which is the same PT_LOAD as .text.
db 'Hello!', 10
.end:
text_end:
db 0 ; Make .data nonempty.
file_end:
mem_end:
Short program with string on the stack (copied from .rodata), write(2) succeeds:
; Compile with: nasm -O0 -o w2 w2.nasm && chmod +x w2
cpu 386
bits 32
org 0x08048000
Ehdr_OSABI equ 0 ; SYSV.
PT:
.LOAD equ 1
.NOTE equ 4
.OPENBSD_SYSCALLS equ 0x65a3dbe9
Elf32_Ehdr: ; ELF-32 i386 header.
db 0x7f, 'ELF', 1, 1, 1, Ehdr_OSABI
db 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0
dd 1, _start, Elf32_Phdr0-$$, 0, 0
dw Elf32_Phdr0-$$, 0x20, (Elf32_Phdr_end-Elf32_Phdr0)>>5
dw 0x28, 0, 0
Elf32_Phdr0: dd PT.LOAD, 0, $$, $$, text_end-$$, text_end-$$, 5, 0x1000
Elf32_Phdr1: dd PT.LOAD, text_end-$$, text_end+0x1000, text_end+0x1000
dd file_end-text_end, mem_end-text_end, 6, 0x1000
Elf32_Phdr2: dd PT.NOTE, Elf32_Note-$$, Elf32_Note, Elf32_Note,
dd Elf32_Note_end-Elf32_Note, Elf32_Note_end-Elf32_Note, 4, 4
Elf32_Phdr3: dd PT.OPENBSD_SYSCALLS, Elf32_osc-$$, 0, 0
dd Elf32_osc_end-Elf32_osc, Elf32_osc_end-Elf32_osc, 4, 4
Elf32_Phdr_end:
Elf32_Note: ; Same PT_NOTE as in /bin/cat in Minix 3.3.0.
dd 8, 4, 1 ; Size of the name, size of the value, node type.
db 'OpenBSD', 0 ; 8-byte name.
dd 0 ; Version number.
Elf32_Note_end:
Elf32_osc: dd syscall_location1, 4, syscall_location2, 1
Elf32_osc_end:
_start:
push byte msg_hello.end-msg_hello
pop edx
sub esp, byte (msg_hello.end-msg_hello+3)&~3
mov edi, esp
mov esi, msg_hello
mov ecx, edx
cld
rep movsd ; Copy msg_hello from .rodata to stack.
mov eax, esp
push edx
push eax
push byte 1 ; STDOUT_FILENO
push eax ; Dummy return adddress.
push byte 4 ; SYS.write.
pop eax
syscall_location1: equ $
int 0x80 ; OpenBSD i386 syscall.
jc short .exit
xor eax, eax ; EXIT_SUCCESS.
.exit:
push eax ; Exit code will be errno from the write(2) above.
push eax ; Dummy return address.
push byte 1 ; SYS.exit.
pop eax
syscall_location2: equ $
int 0x80 ; OpenBSD i386 syscall.
msg_hello: ; We put it to .rodata, which is the same PT_LOAD as .text.
db 'Hello!', 10
.end:
text_end:
db 0 ; Make .data nonempty.
file_end:
mem_end:
Here is some speculation. As @NateEldredge suggested in the comments, maybe the reason for this EFAULT is that the permission check done by write(2) in the OpenBSD >=7.3 kernel decides that the PT_LOAD section is execute-only (and thus refuses to read the buf bytes from it), even though the ELF program header specifies read-execute. A possible workaround for this is creating two PT_LOAD sections: one which is read-only for .rodata, and one which is read-execute for .text (actually the kernel will treat it as execute-only). Existing OpenBSD 7.8 ELF-32 executable programs have this.
Another possible workaround is calling mprotect(2) explicitly with PROT_READ|PROT_EXEC on process startup. This will likely fail, because OpenBSD exeve(2) marks all PT_LOAD sections as mimmutable(2).
This answer is based on the comments on the question. There is no official, explicit documentation about this behavior.
OpenBSD 7.3 has introduced execute-only pages. More specifically, it interprets p_flags (in Elf32_Phdr) for p_type == PT_LOAD like this:
On OpenBSD 7.3--7.8 amd64, the execute-only permission is fully enforced: attempting to read bytes from such a page directly causes a segmentation fault (SIGSEGV), and attempting read bytes from such a page using a syscall (such as write(2)) makes the syscall fail with EFAULT.
On OpenBSD 7.3--7.8 i386, the execute-only permission is partially enforced because of limited CPU support: attempting to read bytes from such a page directly still works (thus not enforced), but attempting read bytes from such a page using a syscall (such as write(2)) makes the syscall fail with EFAULT (thus enforced).
Possible workarounds for OpenBSD >=7.3:
ld does (including on other OSes like GNU/Linux.)rep movsd in w2.nasm in the question. Please note that this may break in future versions of OpenBSD (>7.8).