[HN Gopher] A more robust raw OpenBSD syscall demo
       ___________________________________________________________________
        
       A more robust raw OpenBSD syscall demo
        
       Author : signa11
       Score  : 60 points
       Date   : 2025-03-12 06:11 UTC (16 hours ago)
        
 (HTM) web link (nullprogram.com)
 (TXT) w3m dump (nullprogram.com)
        
       | INTPenis wrote:
       | I'm sorry but I got stuck on the first sentence "Ted Unangst
       | published dude, where are your syscalls? on flak yesterday" and
       | as a long time fediverse operator I got insanely curious about
       | "flak".
       | 
       | So I ended up on the flak tag of this blog[1], but I still can't
       | figure out what it is. I can find no links to any source code, or
       | any service description. Even though the blogger mentions flak
       | being their "signature service".
       | 
       | I'm guessing it's a blogging platform, with ActivityPub support,
       | but I can't find any info about how it's used.
       | 
       | 1. https://flak.tedunangst.com/t/flak
        
         | dontdoxxme wrote:
         | The post you're looking for is about how they also NIH'd a
         | source code browser: https://flak.tedunangst.com/post/humungus
        
         | mrweasel wrote:
         | Flak is a blogging platform... I think. He also has Honk, which
         | is a ActivityPub server?
        
       | debatem1 wrote:
       | Seems like an interesting if maybe not practical protection to
       | implement in eBPF for programs that never make a naked syscall.
       | 
       | Step one would be to ensure that every syscall has a wrapper.
       | Place a uprobe at the start of that wrapper which, when hit, sets
       | a per-thread permission bit and a per-thread-per-syscall
       | permission bit in an eBPF map. Place a corresponding uretprobe
       | that clears the per-thread-per-syscall bit. For each syscall
       | place a kprobe which checks the per-thread table to make sure the
       | thread is one which has enabled the feature, and which then
       | checks to make sure the per-thread-per-syscall bit is set for
       | that syscall. If not, sigkill.
       | 
       | Performance would probably suck but it seems like it would
       | protect the syscall entrypoints enough to do some potentially
       | interesting attack surface reduction. The question is really why
       | you would do that there instead of by attaching to, say, the LSM
       | hooks where you have stronger guarantees vis a vis userspace.
        
         | saagarjha wrote:
         | What's the threat model this protects against?
        
           | debatem1 wrote:
           | "Attacker has ROP and shouldn't be able to make arbitrary
           | syscalls".
           | 
           | Seems mildly useful if you have a really flexible syscall you
           | can't forbid (ioctl, say) but which you only use for a
           | specific narrow purpose.
        
             | saagarjha wrote:
             | If they can ROP they can jump to a syscall instruction with
             | controlled arguments
        
               | wahern wrote:
               | The point of pinsyscall is that they have to jump to the
               | _single_ entry point for that syscall, rather than any of
               | the ~200+ syscall instructions littering the address
               | space. ALSR makes finding an entry point difficult, but
               | that 's easier if you only need to find any syscall
               | instruction, rather than the specific one for the syscall
               | you're invoking. The rationale is explained here: https:/
               | /undeadly.org/cgi?action=article;sid=20230222064027
        
               | debatem1 wrote:
               | The point of what I spelled out above is that they can
               | jump to the instruction but the kernel will kill the
               | program if they don't go through the function up to that
               | point. That allows you to restrict the arguments to the
               | syscall at the point of call.
        
       | oguz-ismail wrote:
       | Why involve C at all? This is much cleaner in assembly
       | .global _start                 .data         what:   .string
       | "hello\n"                 .set    len, .-what - 1
       | .text         _start:                 mov     w0, 1
       | adr     x1, what                 mov     w2, len
       | mov     w8, 4         99:     svc     0                 dsb
       | nsh                 isb                 mov     w8, 1         98:
       | svc     0                 .section        .openbsd.syscalls, ""
       | .long   99b, 4                 .long   98b, 1
       | .section        .note.openbsd.ident, "a"                 .long
       | 8, 4, 1                 .string "OpenBSD"                 .long
       | 0
        
         | hello_computer wrote:
         | especially when confronted with a soup of C flags and
         | declarations, and still not being entirely sure what the C
         | compiler is going to emit
        
           | kccqzy wrote:
           | How is that an issue? You inspect the generated assembler.
           | Godbolt is the industry standard tool for that, but of course
           | just use objdump on the command line if a browser UI isn't
           | your fancy.
           | 
           | Mixing C is helpful because most code doesn't need to be
           | written in raw assembly.
        
             | hello_computer wrote:
             | keeping assembly in asm files, c in c files, compiled by
             | their respective compilers, then linked, has fewer footguns
             | than inline asm
        
               | kccqzy wrote:
               | Yeah but then you have to maintain function interfaces
               | between them in order to link them. The case in this
               | article is for inserting one single asm instruction in an
               | otherwise C codebase.
        
         | pjmlp wrote:
         | Probably due to the audience.
         | 
         | I can grasp the example, but back in my days, if we wanting
         | something faster than interpreted BASIC, Assembly was the way.
         | 
         | Folks nowadays start with Python and JavaScript.
        
         | johnisgood wrote:
         | Isn't "dsb nsh" or "isb" redundant for simple syscalls like
         | write and exit?
        
           | oguz-ismail wrote:
           | Without them it'll segfault on QEMU, IIRC OpenBSD libc uses
           | them invariably as well. I don't know how it fares on real
           | hardware
        
             | johnisgood wrote:
             | Yeah, probably (although I am not sure) because QEMU
             | doesn't fully emulate hardware behavior, but on real
             | hardware some CPUs may internally handle these state
             | transitions better. I suppose its inclusion is recommended
             | to ensure correctness across all CPUs.
             | 
             | It may happen because without "dsb nsh" (Data
             | Synchronization Barrier) and "isb" (Instruction
             | Synchronization Barrier), QEMU may continue execution
             | before the syscall fully completes, which causes a segfault
             | or UB, but I am not entirely sure.
             | 
             | "dsb nsh" and "isb" ensures correct execution order which
             | prevents speculative execution issues.
        
               | pm215 wrote:
               | That doesn't sound right -- QEMU always works one
               | instruction at a time, so we never start executing a
               | following insn until the previous one is completed.
        
               | oguz-ismail wrote:
               | Yeah the segfault doesn't happen on other OSes, I believe
               | it's an OpenBSD thing
        
               | johnisgood wrote:
               | So what causes QEMU to segfault without the
               | synchronization? I have not tested it myself, however, so
               | I cannot guarantee that it indeed does segfault.
        
             | jmillikin wrote:
             | The "dsb nsh; isb" sequence after "svc 0" is part of
             | OpenBSD's mitigations for Spectre.
             | 
             | https://github.com/openbsd/src/commit/bbeaada4689520859307d
             | 5...
             | 
             | https://github.com/openbsd/src/commit/0c401ffc2a2550c32105c
             | e...
             | 
             | https://github.com/openbsd/src/commit/5ecc9681133f1894e81c3
             | 8...
             | 
             | If I'm reading the commits correctly, the OpenBSD kernel
             | will skip two instructions after a "svc 0" when returning
             | to userspace, on the assumption that any syscall comes from
             | libc and therefore has "dsb nsh; isb" after it.
        
       | rollcat wrote:
       | I like how Go provides the "syscall" package in the standard
       | library. It's OS/ARCH specific, and takes every precaution to
       | call the raw thing safely - notably syscall.ForkExec on UNIX
       | platforms (digging thru the source made me really appreciate the
       | scope of their work).
       | 
       | It "feels" very low-level in an otherwise very high-level
       | language, but provides enough power to implement an entire
       | userland from initrd up, libc- and asm-free (check out Gokrazy
       | and u-root).
       | 
       | OpenBSD and Go have always been at odds here. Go really wants to
       | produce static executables whenever possible, and do syscalls
       | directly; OpenBSD really wants to prevent user programs from
       | accidentally creating gadgets. I guess they've settled on
       | dynamically linking ld.so?
        
         | actionfromafar wrote:
         | And the MacOS API is similar.
        
           | rollcat wrote:
           | It's a bit more complicated. Ideally you'd only need
           | libdyld.dylib and libSystem.B.dylib or so, but...
           | $ echo 'package main; func main() {}' > nop.go         $ go
           | build ./nop.go         $ DYLD_PRINT_LIBRARIES=1 ./nop 2>&1 |
           | wc -l               44
           | 
           | OpenBSD & macOS philosophies are often surprisingly aligned
           | in certain ways, but OpenBSD is simple, macOS is
           | comprehensive (and - TBF quite bloated).
        
             | nxobject wrote:
             | That's a very fair assessment of Apple - IIRC a lot of
             | early idiosyncratic hardening work in XNU world was done
             | for the launch of the iPhone OS App Store, but it's only
             | been (relatively) recently that they've started long-term
             | initiatives systematically harden macOS on top.
        
       | fwsgonzo wrote:
       | The inline assembly is not idiomatic. Today you should be using
       | register asm. Here is a RISC-V example:
       | register long a0 asm("a0") = arg0;         register long
       | syscall_id asm("a7") = n;              asm volatile ("ecall" :
       | "+r"(a0) : "r"(syscall_id));              return a0;
       | 
       | This is an example where a0 is an in/out integer. For memory,
       | change long a0 to a pointer to some struct and add a "m" input or
       | "+m" in/out. It's even easier in other languages like Rust and
       | Zig.
        
         | sim7c00 wrote:
         | you can also out register preferences directly into the asm
         | volatile line.
         | 
         | asm ( assembler template : output operands (optional) : input
         | operands (optional) : clobbered registers list (optional) );
         | 
         | clobbered register list will give same hints to compiler...
         | register keyword is also a hint not a fixed thing so i cant
         | imagine its really handled differently?
         | 
         | might be wrong, but it seem earier to me to use all the
         | features of the asm call itself rather than outside of it pre-
         | specifying these preferences.
        
         | pjmlp wrote:
         | Actually nowadays ideally we would already caught up with
         | ESPOL/NEWP from 1961 and use only intrisics.
         | 
         | Apparently that is a VC++ only thing.
        
           | linksnapzz wrote:
           | MCP and the rest of the Burroughs Large Systems are an
           | interesting road not taken.
        
         | LegionMammal978 wrote:
         | > The inline assembly is not idiomatic. Today you should be
         | using register asm.
         | 
         | Who's writing these idioms? I've always seen register variables
         | as an alternative, not the default option, except for those
         | registers which have no constraint code.
        
           | fuhsnn wrote:
           | Probably the current state that GCC for Aarch64 and RISC-V
           | simply don't offer individual register constraints like x86.
        
           | fwsgonzo wrote:
           | Register variables are idiomatic when you're locking down
           | every single register anyway, which is always the case for
           | system calls. If you're writing assembly where you want the
           | register allocators help, by all means use regular inline
           | assembly.
        
         | saagarjha wrote:
         | I'd really prefer using inline assembly tbh
        
           | fwsgonzo wrote:
           | This is inline assembly.
        
             | saagarjha wrote:
             | Normal inline assembly not the register pinning you're
             | doing
        
         | armitron wrote:
         | Idiomatic to you perhaps, to me this looks ridiculous and
         | unnecessary, bordering on masochistic.
        
       | sim7c00 wrote:
       | maybe to make it more concise binary you can do something like
       | nostdinc nostdlib or freestanding and add own linker file to be
       | more explicit about how binary is build and what to /discard/.
       | also max page size and other linker flags can impact. its
       | sometimes a bit trial and error to get it to spit out a small
       | binary with only the code u really wrote.
       | 
       | own linker file and manual linking step imo is key. (i use
       | gcc/ld). if u let gcc spit linker file for modern platform, u can
       | see its full of clutter... most of it unneeded. u can also strip
       | that one down, but i am sure u know what elf sections to put and
       | omit, and you found all the bsd specific ones.
       | 
       | in the linker step u can also add symbols / calculate offsets.
       | 
       | in gcc u can also in the c code use attribute section('bla'). not
       | sure if its handy in this case but maybe it'll ease somewhat
       | these things or bring it back more into C :).
       | 
       | cool example :) remebering struggle tirleesly tryin to find out
       | how to run a raw syscall on openbsd. a lot of man pages, readelfs
       | and headaches i was so happy to get my exit code hahah
        
       ___________________________________________________________________
       (page generated 2025-03-12 23:01 UTC)