EMILUA(3em)

πŸ“¦ version 0.12.1, in nixos 26.05, nixpkgs unstable

🌸 NAME 🌸

emilua(3em) β€” Emilua - Lua execution engine πŸ’–

✨ SYNOPSIS ✨

local libc_service = require "libc_service"

πŸ’– DESCRIPTION πŸ’–

Okay so bestie, here’s the vibe: the master arbitrates calls to libc that are related to ambient authority in the process holding the slave end. Before a call to the real libc is attempted in the slave end, the process forwards the request to the master and blocks the thread until a reply is received. 🫢

The protocol follows the request-reply model. Low-level protocol details are hidden by the Emilua runtime, and the Lua programmer only sees a request-reply API. ✨

Multiplexing is not allowed. That means only one thread from the slave end can be served at anytime, and this layout will minimize the opportunity for possible parallelism if the process holding the slave end makes too many calls for ambient authority access. 😭 As a hidden implementation detail, Emilua will transparently pipeline requests from different threads to minimize latency a little. πŸ’…

πŸŽ€ FUNCTIONS πŸŽ€

πŸ’… receive(self) πŸ’…

Receive the next attempted libc call for ambient authority. 🌟

Data about the call is stored in the object’s properties. πŸ¦‹

send(self, result: value, errno: integer|generic_error|system_error = 0)

Okay so this is the one you call when you're the one answering the phone πŸ“ž β€” like, you're the handler for the libc call that's currently being arbitrated, and send() is how you hand the answer back. The signature is literally:

send(self, result: value, errno: integer|generic_error|system_error = 0)

Check the default on that errno bestie: it's already 0 for you, so if everything went fine you literally don't have to type it πŸ’…. And errno can be a plain integer, a generic_error, or a system_error β€” pick your fighter.

Now, what even is result? Depends which call you're answering. Here's the whole menu, no cap πŸ“:

  • open β†’ integer
  • openat β†’ integer
  • unlink β†’ integer
  • rename β†’ integer
  • stat, lstat β†’ this one's special, see below πŸ‘€
  • access, eaccess β†’ integer
  • mkdir β†’ integer

Lowkey a lot of integers. It's giving "just give me a number" energy and honestly? Respect. πŸ”₯


the stat / lstat situation πŸ’€

These two are the divas of the group. Not an integer.

  • On error, it should be the number -1. That's it. That's the whole vibe. 😭
  • On success, it should be an object (table) with these properties, and I need you to get every single one right because this is the exact struct:
    • dev: number β€” Device ID.
    • ino: number β€” Inode number.
    • mode: number β€” File access mode bits.
    • type: "regular"|"directory"|"symlink"|"block"|"character"|"fifo"|"socket" β€” File type. Yes, exactly one of those seven strings, don't get creative ✨
    • nlink: number β€” Number of hardlinks.
    • uid: number β€” User ID of owner.
    • gid: number β€” Group ID of owner.
    • rdev: number β€” Like dev, but for special files.
    • size: number β€” Total size, in bytes.
    • atime: filesystem.clock.time_point β€” The time of the last access.
    • mtime: filesystem.clock.time_point β€” The time of the last modification.
    • ctime: filesystem.clock.time_point β€” The time of the last status change.
    • blksize: number β€” Block size for filesystem I/O.
    • blocks: number β€” Number of blocks allocated.

Bestie the way there are three separate time fields and they all want filesystem.clock.time_point β€” atime, mtime, ctime, all iconically distinct, all required, don't skip one thinking nobody will notice πŸ™ˆ

Fr fr: on error it's the number -1, not nil, not false, not a vibe. -1. Periodt. πŸ‘‘

Go ahead and fill them all in and send() it back like the main character you are 🫢

…and the return-type table just keeps going, bestie, so buckle up πŸŽ€ every single one of these returns an integer unless it says otherwise:

  • rmdir β€” returns an integer, periodt.
  • connect_unix β€” also an integer.
  • connect_inet β€” integer.
  • connect_inet6 β€” integer.
  • bind_unix β€” integer.
  • bind_inet β€” integer.
  • bind_inet6 β€” integer.

πŸ’œ getaddrinfo β€” the one with the extra lore

This icon does not just give you a plain integer, no ma'am. Its return value has ✨rules✨ and you're gonna learn them.

When it fails, the value should be one of these exact strings, ok? Like, literally character for character β€” no freestyling πŸ™ˆ

  • "again"
  • "badflags"
  • "fail"
  • "family"
  • "memory"
  • "noname"
  • "service"
  • "socktype"
  • "system"

When it succeeds, it should be an array with the following members, and yes, they come in the same order:

  • ip: ip.address β€” the address the query resolved to. That's it, that's the whole vibe πŸ’«
  • service: integer|nil β€” the service port the query resolved to. Or nil, she's flexible like that.
🫢 alternatively β€” and this is a valid scenario for DNS and getaddrinfo(), not a bug, don't come for me β€” if the call should succeed with a reply of 0 elements, you can use the value nil instead. Zero elements is a whole mood and she's allowed πŸ’…

✨ send_with_fds

send_with_fds(self, result: value, fds: file_descriptor[], errno:
    integer|generic_error|system_error = 0)

Okay bestie, this is the one where you actually hand the answer back πŸ’… β€” it sends the result for the libc call that's currently being arbitrated. So whichever poor little syscall is sitting there waiting on you, this is how you tell it what happened.

You pass self, you pass the result (and what counts as a valid result depends entirely on which function you're answering for β€” see the receipts below πŸ‘‡), you pass fds as a file_descriptor[], and errno defaults to 0 and can be an integer, a generic_error, or a system_error.

So here's the whole "which function wants which result" situation, laid out like a group chat roll call 🫢:

  • open β€” result is an integer.
  • openat β€” result is an integer.
  • unlink β€” result is an integer.
  • rename β€” result is an integer.
  • stat, lstat β€” okay this one's the main character, pay attention πŸ‘‘
    • On error, it should be the number -1.
    • On success, it should be an object (table) with these properties:
      • dev: number β€” Device ID.
      • ino: number β€” Inode number.
      • mode: number β€” File access mode bits.
      • type: "regular"|"directory"|"symlink"|"block"|"character"|"fifo"|"socket" β€” File type.
      • nlink: number β€” Number of hardlinks.
      • uid: number β€” User ID of owner.
      • gid: number β€” Group ID of owner.
      • rdev: number β€” Like dev, but for special files.
      • size: number β€” Total size, in bytes.
      • atime: filesystem.clock.time_point β€” The time of the last access.
      • mtime: filesystem.clock.time_point β€” The time of the last modification.
      • ctime: filesystem.clock.time_point β€” The time of the last status change.
      • blksize: number β€” Block size for filesystem I/O.
      • blocks: number β€” Number of blocks allocated.
  • access, eaccess β€” result is an integer.
  • mkdir β€” result is an integer.
  • rmdir β†’ integer πŸŽ€
  • connect_unix β†’ integer πŸŽ€
  • connect_inet β†’ integer πŸŽ€
  • connect_inet6 β†’ integer πŸŽ€
  • bind_unix β†’ integer πŸŽ€
  • bind_inet β†’ integer πŸŽ€
  • bind_inet6 β†’ integer πŸŽ€
  • getaddrinfo β†’ okay bestie, this one's the main character of the column, so buckle up πŸ’…

For getaddrinfo, if it errors, the value should be one of these strings:

  • "again"
  • "badflags"
  • "fail"
  • "family"
  • "memory"
  • "noname"
  • "service"
  • "socktype"
  • "system"

And on success it should be an array with these members, in exactly this order, no shuffling πŸ”₯

  • ip: ip.address β€” the address the query resolved to.
  • service: integer|nil β€” the service port the query resolved to.

And alternatively β€” if the call should succeed but come back with a reply of 0 elements (which, yes, is a totally valid scenario for DNS and getaddrinfo()), you can just use the value nil instead. 🌸


✨ use_slave_credentials(self) ✨

Forward the request to the real libc running in the slave end. Like passing a note to the actual adult in the room 🫢

✨ arguments(self) β†’ value... ✨

The arguments passed to the last requested call.

The arguments depend on whichever function got called (peep the function_ property below πŸ‘€).

🌈 open

  • path: filesystem.path β€” the file path.
  • flags: string[] β€” the open flags.

flags may contain:

  • "append" β€” open the file in append mode.
  • "create" β€” create the file if it doesn't exist.
  • "directory" β€” if the pathname isn't a directory, cause the open to fail.
  • "exclusive" β€” make sure a new file gets created. Has to be combined with "create".
  • "no_follow" β€” fail if the path resolves to a symbolic link.
  • "path" β€” get a stable reference to an inode without actually opening the contents.
  • "read_only" β€” open the file for reading.
  • "read_write" β€” open the file for reading and writing.
  • "sync_all_on_write" β€” open the file so that write operations automatically synchronise the file data and metadata to disk (FILE_FLAG_WRITE_THROUGH/O_SYNC).
  • "temporary" β€” create an unnamed temporary regular file.
  • "truncate" β€” open the file with any existing contents truncated.
  • "write_only" β€” open the file for writing.

And then:

  • mode: integer β€” optional argument, bestie. Only present if "create" or "temporary" show up in flags. ✨

🌈 openat

  • path: filesystem.path β€” the file path.
  • flags: string[] β€” the open flags.

flags may contain:

  • "append" β€” open the file in append mode.
  • "create" β€” create the file if it doesn't exist.
  • "directory" β€” if the pathname isn't a directory, cause the open to fail.
  • "exclusive" β€” make sure a new file gets created. Has to be combined with "create".
  • "no_follow" β€” fail if the path resolves to a symbolic link.
  • "path" β€” get a stable reference to an inode without actually opening the contents.
  • "read_only" β€” open the file for reading.
  • "read_write" β€” open the file for reading and writing.
  • "sync_all_on_write" β€” open the file so that write operations automatically synchronise the file data and metadata to disk (FILE_FLAG_WRITE_THROUGH/O_SYNC).
  • "temporary" β€” create an unnamed temporary regular file.
  • "truncate" β€” open the file with any existing contents truncated.
  • "write_only" β€” open the file for writing.
  • "resolve_beneath" β€” path resolution must not cross the fd directory.
  • "resolve_in_root" β€” treat the directory referred to by dirfd as the root directory while resolving pathname. Absolute symbolic links get interpreted relative to dirfd. πŸŒ€
  • "resolve_no_magiclinks" β€” disallow all magic-link resolution during path resolution.
  • "resolve_no_symlinks" β€” disallow resolution of symbolic links during path resolution.
  • "resolve_no_xdev" β€” disallow traversal of mount points during path resolution (including all bind mounts).
  • "resolve_cached" β€” make the open operation fail unless all path components are already chilling in the kernel's lookup cache.

And then, same deal as before:

  • mode: integer β€” optional argument. Only present if "create" or "temporary" appear in flags. πŸ’–

…and unlink is where this part runs out of runway β€” the rest of it lives in the next chunk, so hold that thought, bestie πŸ¦‹

  • path: filesystem.path β€” the file path πŸ›€οΈ

rename

  • path1: filesystem.path β€” the file1 path (the one you're moving from, bestie) πŸ“€
  • path2: filesystem.path β€” the file2 path (the one you're moving to) πŸ“₯

stat, lstat

  • path: filesystem.path β€” the file path πŸ”
ok so heads up, these two share the exact same param list β€” stat follows symlinks, lstat doesn't, but that ✨lore✨ lives in the C docs, not here. All this page is telling you is: one path, type filesystem.path.

access, eaccess

  • path: filesystem.path β€” the file path πŸ“
  • amode: "f"|string[] β€” the requested access mode 🎚️

It's either "f", or an array of strings that may contain any of these lil guys:

  • "r" β€” R_OK. Read permission πŸ“–
  • "w" β€” W_OK. Write permission ✍️
  • "x" β€” X_OK. Execute permission πŸƒ
ngl the whole "f"-or-array thing is giving "pick a lane", but it's literally what the page says, so πŸ’…

mkdir

  • path: filesystem.path β€” the file path πŸ“‚
  • mode: integer β€” file access mode bits πŸ”’

rmdir

  • path: filesystem.path β€” the file path πŸ—‘οΈ

connect_unix

  • path: filesystem.path β€” the UNIX socket path πŸ”Œ

connect_inet

  • ipv4_addr: ip.address β€” the IPv4 address 🌐
  • port: integer β€” the port πŸšͺ

connect_inet6

  • ipv6_addr: ip.address β€” the IPv6 address 🌐
  • port: integer β€” the port πŸšͺ

bind_unix

  • path: filesystem.path β€” the UNIX socket path πŸͺ’

bind_inet

  • ipv4_addr: ip.address β€” the IPv4 address 🌐
  • port: integer β€” the port πŸšͺ

bind_inet6

  • ipv6_addr: ip.address β€” the IPv6 address 🌐
  • port: integer β€” the port πŸšͺ

getaddrinfo

  • node: string β€” an Internet host such as host.example πŸ–₯️
  • service: string β€” an Internet service such as http 🧾
  • protocol: "tcp"|"udp"|nil β€” an Internet protocol. Only really useful if you're resolving the service as well 🎯

descriptors(self) β†’ file_descriptor...

Okay bestie, this one is the payoff πŸ’– it extracts and returns the file descriptors that came in with the last requested call. Like, it goes and grabs them out of the object for you. Iconic behaviour, honestly.

⚠️ Note β€” once you call this function, the returned descriptors no longer stay stored in this object. IOW: a second call to this function will always return nothing. So don't call it twice expecting seconds, you will simply get crumbs 😭 grab them once and hold on tight 🫢

✨ PROPERTIES ✨

function_: string

The last requested call πŸ—‚οΈ β€” literally just a string telling you which one you asked for last.

Possible values, in order, no skips, we're listing all of them πŸ’…:

  • "open"
  • "openat"
  • "unlink"
  • "rename"
  • "stat"
  • "lstat"
  • "access"
  • "eaccess"
  • "mkdir"
  • "rmdir"
  • "connect_unix"
  • "connect_inet"
  • "connect_inet6"
  • "bind_unix"
  • "bind_inet"
  • "bind_inet6"
  • "getaddrinfo"

and that's the whole page, bestie β€” you ate that πŸ“πŸ’…

This one was Emilua 0.12.1-nixpkgs1, dated 1980-01-01 (yes the date is a whole vibe, we don't question it πŸ™ˆ). Go forth and connect_inet6 responsibly, ily βœ¨πŸŽ€πŸ¦‹

πŸ“¦ where this page comes from