x86linkernasmelfopenbsd

write(2) syscall returns EFAULT for string in .rodata on OpenBSD 7.3--7.8


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:

FYI Changelog of OpenBSD 7.3.


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).


Solution

  • 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: