Exposes ioctl() and fcntl() to php-land.
PHP talks to files, sockets and pipes through streams, and a stream hides the file descriptor on purpose. For a file that's fine. But if you want to play with, say a UART or I2C sensor, that's not going to work. Sure, you can fopen('/dev/ttyAMA0') but you can't configure the port, and you can't ioctl() anything. If you want to work with hardware, you usually need to learn C or Python and use their libraries. Or even less-viable for production: FFI into those runtimes, increasing what you ship.
ext/posix, which ships with PHP, has the process-level calls and stops before open(2), read(2), ioctl(2), fcntl(2), termios and wait(2). Those are the POSIX calls hardware needs.
posi is the rest of ext/posix: the descriptor, device-control, terminal and process-wait calls, bound one to one under the names C uses, with the platform's own constant values compiled in. Though like ext-posix, it does not yet bind all of the POSIX library. It does, however bind what talking to a device node requires, and it never redefines a function ext/posix already has.
ioctl() and fcntl() are the two that open the door. Every driver that shows up as a node under /dev is driven through them: gpio chips, i2c and spi buses, serial ports, cameras. A C library that is only a wrapper over those calls, libgpiod being the obvious one, can be reimplemented in PHP on top of these two with no C dependency at all.
The only problem? Without an extension the way to reach these calls is FFI: parse a header, declare every struct by hand, guess the constant values per platform, and run with ffi.enable on. posi needs none of that. The constants come from the headers at build time, every argument is checked against the C type it lands in, it loads under NTS, ZTS and opcache preloaded like any other extension.
Most ways you will interface with posi are in two parts -
Open the device node with posix_open() and get back its file descriptor. Then tell the driver what you want with ioctl(): the request number and the shape of its third argument come from the driver's header, and the shape of $arg tells posi how to pass it. Here I2C_SLAVE takes the address as the argument itself, so an int goes straight through.
const I2C_SLAVE = 0x0703; // from <linux/i2c-dev.h>
// Open the bus and get back a raw file descriptor, not a stream.
$fd = posix_open('/dev/i2c-1', O_RDWR);
if ($fd < 0) {
throw new RuntimeException('open: errno ' . posi_errno());
}
// Point the bus at one device for every read and write that follows.
if (ioctl($fd, I2C_SLAVE, 0x53, $unused) < 0) {
throw new RuntimeException('I2C_SLAVE: errno ' . posi_errno());
}Talk to it: move bytes with posix_write() and posix_read(), exactly as C would. Close it yourself when you're done; nothing closes a descriptor for you.
// Write the register number, read the byte at it.
posix_write($fd, "\x00", 1);
$byte = posix_read($fd, 1);
if ($byte === false) {
// The read failed; posi_errno() says why
$this->handle(posi_errno());
}
else
{
printf("DEVID 0x%02X\n", ord($byte)); // 0xE5 on an ADXL343
}
posix_close($fd);A serial port is configured through termios rather than ioctl(): pull the port's struct, edit it in place, push it back, then wait for bytes with posix_ppoll().
$fd = posix_open('/dev/ttyAMA0', O_RDWR | O_NOCTTY | O_NONBLOCK);
if ($fd < 0) {
throw new RuntimeException('open: errno ' . posi_errno());
}
$termios = tcgetattr($fd);
$termios['c_iflag'] &= ~(IXON | IXOFF | ICRNL);
$termios['c_oflag'] &= ~OPOST;
$termios['c_lflag'] &= ~(ICANON | constant('ECHO') | ISIG | IEXTEN);
$termios['c_cflag'] = ($termios['c_cflag'] & ~(CSIZE | PARENB | CSTOPB)) | CS8 | CLOCAL | CREAD;
tcsetattr($fd, TCSANOW, $termios);
posi_set_baud_rate($fd, 115200);
posix_write($fd, "AT\r\n", 4);
while (true)
{
// 1 = readable, 0 = timed out after 2 s, -1 = error with posi_errno() set
$ready = posix_ppoll($fd, 2_000_000_000, POLLIN);
if ($ready === 1) {
$bytes = posix_read($fd, 256);
$this->handle($bytes);
}
else
{
// Nothing arrived in time; stop listening and give the descriptor back
posix_close($fd);
break;
}
}| OS | Linux or macOS. Every call is POSIX; the two Linux-only pieces (ppoll(2) and the termios2 custom baud path) have a POSIX fallback on macOS. Windows is excluded. |
| PHP | 8.4 or newer, NTS or ZTS. |
| Extensions | ext/posix, which ships with PHP. posi requires it and loads after it, because posix_getuid() and posix_setuid() come from there under the same names. Built in on Homebrew PHP; a shared posix.so on Debian packaging, enabled by default. |
| Libraries | None beyond libc. |
| Optional | ext/pcntl, ext/sockets, ext-kqueue. Not needed; when present, posi loads after them so constants they share are registered once. |
To build it you need a C compiler and the development files of the PHP you intend to load it into:
# Debian, Ubuntu, Raspberry Pi OS
sudo apt install build-essential php8.4-dev
# macOS: Xcode Command Line Tools, and Homebrew's PHP ships phpize and php-config
xcode-select --install
brew install php@8.4PIE is the PHP Foundation's installer for extensions. It downloads the tagged source from Packagist, builds it against the PHP on your PATH, installs the module and enables it. sudo where your PHP's extension directory is root-owned, as on Debian; not on a Homebrew Mac:
pie install php-io-extensions/posiThe repository ships two installers, one per platform. Each one builds in a temporary copy of the checkout for each PHP it targets, installs posi.so into that PHP's extension_dir, writes 30-posi.ini into its conf.d scan directory, checks the module loads, and leaves the checkout clean. The macOS one also ad-hoc signs the module so the system loads it on first use.
git clone https://github.com/php-io-extensions/posi.git
cd posi
./install-debian-trixie.sh # Debian Trixie, Ubuntu 24.04+, Raspberry Pi OS: the php on PATH
./install-macos.sh # Homebrew php@8.4 and php@8.4-zts
./install-macos.sh /path/to/bin/php # either script takes specific PHP binariesgit clone https://github.com/php-io-extensions/posi.git
cd posi
phpize
./configure --enable-posi
make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)"
sudo make installThen enable it: add extension=posi.so to your php.ini, or drop that line into a file in the scan directory that php --ini reports. On Debian packaging that file must sort after the one that loads posix, which 30-posi.ini does next to 20-posix.ini.
$ php --ri posi
posi
posi support => enabled
Version => 0.10.xEvery function is a global, named as C names it. A failed call returns what C returns, -1 or false, and keeps errno for posi_errno(). A bad PHP argument, an int wider than the C type or a length past the end of the data, throws ValueError or TypeError before the syscall; nothing is truncated. Constants (O_*, F_*, POLL*, S_IF*, TCSA*, B*, the termios flag bits, W*, E*) carry the platform's own values from its headers.
Opens a path and returns a file descriptor, or -1 (same semantics as C open(2)).
$device_path— device node, FIFO, socket path, or regular file.$flags—O_*access mode and creation flags, such asO_RDWR | O_NOCTTY | O_NONBLOCK.$mode— permission bits applied whenO_CREATis set, under the process umask.
Example
$fd = posix_open('/dev/i2c-1', O_RDWR);
if ($fd < 0) {
throw new RuntimeException('open: errno ' . posi_errno());
}Closes a descriptor. Returns 0, or -1 (same semantics as C close(2)). Nothing else closes a descriptor posi opened; a descriptor left open is reclaimed at process exit.
Example
posix_close($fd);Reads up to $bytes_to_read bytes. Returns the bytes read, "" at end of file, or false (same semantics as C read(2)). A negative length throws ValueError.
Example
$chunk = posix_read($fd, 256);
if ($chunk === false) {
throw new RuntimeException('read: errno ' . posi_errno());
}Writes the first $bytes_to_write bytes of $data. Returns the number written, or -1 (same semantics as C write(2)). A length past the end of $data throws ValueError.
Example
$written = posix_write($fd, "\x00\x2D\x08", 3);
if ($written !== 3) {
throw new RuntimeException('short write: ' . $written);
}Repositions the descriptor's offset. Returns the new offset from the start of the file, or -1 (same semantics as C lseek(2)). $whence is SEEK_SET, SEEK_CUR or SEEK_END.
Example
$fd = posix_open('/tmp/posi-demo.bin', O_RDWR | O_CREAT | O_TRUNC, 0644);
posix_write($fd, "hello world", 11);
posix_lseek($fd, 6, SEEK_SET);
echo posix_read($fd, 5); // world
posix_close($fd);Reads into several buffers in one call. $iovecs is a list of ['len' => int]; the result is ['res' => total bytes, 'buffers' => [string, ...]] with each buffer cut to the bytes that landed in it, or false (same semantics as C readv(2)). More than IOV_MAX entries, or a len that is missing, negative or not an int, throws ValueError.
Example
$fd = posix_open('/tmp/posi-demo.bin', O_RDONLY);
$io = posix_readv($fd, [['len' => 5], ['len' => 6]]);
// $io['res'] === 11, $io['buffers'] === ['hello', ' world']
posix_close($fd);Receives up to $len bytes from a socket descriptor. Returns the bytes, or false (same semantics as C recv(2)). $flags takes MSG_PEEK, MSG_WAITALL, MSG_OOB.
Example
// $sock is a connected socket descriptor
$peek = posix_recv($sock, 4, MSG_PEEK); // look without consuming
$data = posix_recv($sock, 4); // now consumeWaits for one descriptor to become ready. Returns 1 when any requested event (or POLLHUP/POLLERR) is pending, 0 on timeout, or -1 (same semantics as C ppoll(2); poll(2) on macOS, which rounds the timeout up to whole milliseconds).
$timeout_ns— nanoseconds to wait;0returns at once, a negative value waits forever.$events— aPOLL*mask;0meansPOLLIN.
To wait on many descriptors at once, use ext-epoll or ext-kqueue instead.
Example
if (posix_ppoll($fd, 2_000_000_000, POLLIN) === 1) { // up to 2 s
$bytes = posix_read($fd, 256);
}Wraps a descriptor as a PHP stream resource, or returns false (same semantics as C fdopen(3)). The descriptor is not duplicated: the stream owns it, fclose() closes it, and the stream works with fgets(), stream_select() and everything else that takes a stream. A descriptor that is not open fails with errno set.
$mode— a PHP stream mode such as'r','w'or'r+'.
Example
$fd = posix_open('/dev/ttyAMA0', O_RDWR | O_NOCTTY);
$port = posix_fdopen($fd, 'r+');
$line = fgets($port);
fclose($port); // closes $fd tooPerforms a descriptor control operation and returns the C result (same semantics as C fcntl(2)).
$cmd— anF_*command.$arg— an int, a bool, ornullfor commands that take no argument. ForF_GETLK,F_SETLKandF_SETLKW, an array['type', 'whence', 'start', 'len', 'pid']describing astruct flock; missing keys count as0. Any other type throwsTypeError.$value— on success receives the datum forF_GETFD,F_GETFLandF_GETOWN, the resulting lock array for lock commands, and a copy of$argotherwise. On failure it receivesfalse.
Example — make a descriptor non-blocking
fcntl($fd, F_GETFL, null, $flags);
fcntl($fd, F_SETFL, $flags | O_NONBLOCK, $unused);Example — take a write lock on a file
$lock = ['type' => F_WRLCK, 'whence' => SEEK_SET, 'start' => 0, 'len' => 0];
if (fcntl($fd, F_SETLK, $lock, $result) < 0 && posi_errno() === EAGAIN) {
// someone else holds it
}Performs a device-specific operation and returns the C result (same semantics as C ioctl(2)). $request is passed as unsigned long unchanged, so Linux _IOR/_IOW numbers above INT_MAX work. The shape of $arg decides how the third C argument is passed:
$arg |
C receives | $value after success |
|---|---|---|
['value' => int] |
a pointer to that int | the int, as the kernel left it |
['bytes' => string], ['data' => string], or a plain string |
a pointer to a private copy of the bytes | the copy, as the kernel left it |
| an int or bool | the number itself, as the pointer-sized argument | a copy of $arg |
null |
NULL |
the return value when positive, else null |
On failure $value is false. An array with none of those keys throws ValueError; any other type throws TypeError. The driver's header says which shape each request wants; posi cannot know. An int that is an address goes to the kernel as an address, which is what passing a posi_mem_alloc() buffer relies on.
Example — a request that takes a number
const I2C_SLAVE = 0x0703;
ioctl($fd, I2C_SLAVE, 0x53, $unused);Example — a request that fills an int
// FIONREAD: how many bytes are waiting to be read
const FIONREAD = PHP_OS_FAMILY === 'Darwin' ? 0x4004667F : 0x541B;
ioctl($fd, FIONREAD, ['value' => 0], $waiting);Example — a request that reads and writes a struct
// SPI_IOC_RD_MODE on Linux: one byte in, one byte out
const SPI_IOC_RD_MODE = 0x80016B01;
ioctl($fd, SPI_IOC_RD_MODE, ['bytes' => "\x00"], $mode);
echo ord($mode);Sets the permission bits on a path. Returns 0, or -1 (same semantics as C chmod(2)).
Example
if (posix_chmod('/dev/gpiochip0', 0660) < 0) {
throw new RuntimeException('chmod: errno ' . posi_errno());
}Like posix_chmod(), on an open descriptor (same semantics as C fchmod(2)).
Example
posix_fchmod($fd, 0600);Sets the owner and group of a path. Returns 0, or -1 (same semantics as C chown(2)). Pass -1 for either to leave it unchanged.
Example
posix_chown('/tmp/posi-demo.bin', posix_getuid(), -1);Like posix_chown(), on an open descriptor (same semantics as C fchown(2)).
Example
posix_fchown($fd, -1, posix_getgid());Sets the file mode creation mask and returns the previous one (same semantics as C umask(2)).
Example
$previous = posix_umask(0077);
$fd = posix_open('/tmp/posi-private.bin', O_RDWR | O_CREAT, 0666); // lands as 0600
posix_umask($previous);Stats a path without following a final symlink. Returns the stat array (dev, ino, mode, nlink, uid, gid, rdev, size, atime, mtime, ctime, blksize, blocks), or false (same semantics as C lstat(2)).
Example
$st = posix_lstat('/dev/i2c-1');
if ($st !== false && ($st['mode'] & S_IFMT) === S_IFCHR) {
// a character device
}Returns the host name, or false (same semantics as C gethostname(3)).
Example
echo posix_hostname();Waits for any child to change state. Returns the child's pid, or -1; $status receives the raw wait status (same semantics as C wait(2)).
Example
$pid = posix_wait($status);
if ($pid > 0 && posix_wifexited($status)) {
echo "child {$pid} exited with ", posix_wexitstatus($status), "\n";
}Waits for one child, or any child when $pid is -1. Returns the pid, 0 when WNOHANG is set and nothing has changed, or -1 (same semantics as C waitpid(2)). $options takes WNOHANG, WUNTRACED, WCONTINUED.
Example
// poll a child without blocking
if (posix_waitpid($child, $status, WNOHANG) === $child) {
// it is gone; $status says how
}posix_wifexited(int $status): bool, posix_wexitstatus(int $status): int, posix_wifsignaled(int $status): bool, posix_wtermsig(int $status): int, posix_wifstopped(int $status): bool, posix_wstopsig(int $status): int, posix_wifcontinued(int $status): bool
The <sys/wait.h> status macros, applied to a $status from posix_wait() or posix_waitpid().
Example
$pid = posix_waitpid(-1, $status);
match (true) {
posix_wifexited($status) => printf("exit %d\n", posix_wexitstatus($status)),
posix_wifsignaled($status) => printf("killed by signal %d\n", posix_wtermsig($status)),
posix_wifstopped($status) => printf("stopped by signal %d\n", posix_wstopsig($status)),
default => print("continued\n"),
};A terminal's settings travel as one array mirroring struct termios: c_iflag, c_oflag, c_cflag, c_lflag (ints), c_cc (a list of NCCS ints, indexed by VMIN, VTIME, ...), and c_ispeed, c_ospeed. Every function that takes the array requires every key; the speeds are there because macOS keeps them outside c_cflag, and a round trip without them would apply B0 and hang up the line. ECHO is a PHP keyword, so that flag is read with constant('ECHO').
Reads the terminal settings of a descriptor into the array, or returns false (same semantics as C tcgetattr(3)).
Example
$termios = tcgetattr($fd);
if ($termios === false) {
throw new RuntimeException('not a terminal: errno ' . posi_errno());
}Applies the array to the descriptor. Returns 0, or -1 (same semantics as C tcsetattr(3)). $action is TCSANOW, TCSADRAIN or TCSAFLUSH. A Linux custom rate set through BOTHER survives a tcgetattr() → edit → tcsetattr() round trip.
Example — raw mode, 8N1
$termios = tcgetattr($fd);
$termios['c_iflag'] &= ~(IXON | IXOFF | ICRNL);
$termios['c_oflag'] &= ~OPOST;
$termios['c_lflag'] &= ~(ICANON | constant('ECHO') | ISIG | IEXTEN);
$termios['c_cflag'] = ($termios['c_cflag'] & ~(CSIZE | PARENB | CSTOPB)) | CS8 | CLOCAL | CREAD;
$termios['c_cc'][VMIN] = 0;
$termios['c_cc'][VTIME] = 10; // tenths of a second
tcsetattr($fd, TCSANOW, $termios);cfsetispeed(array $termios, int $speed): array|false, cfsetospeed(array $termios, int $speed): array|false
Return the array with the input or output speed set to a B* constant, or false when the platform refuses the speed (same semantics as C cfsetispeed(3)/cfsetospeed(3)). The array is returned, not modified in place.
Example
$termios = cfsetispeed($termios, B9600);
$termios = cfsetospeed($termios, B9600);
tcsetattr($fd, TCSANOW, $termios);Return the input or output speed held in the array, as a B* value (same semantics as C cfgetispeed(3)/cfgetospeed(3)).
Example
if (cfgetospeed(tcgetattr($fd)) === B115200) {
// already at 115200
}Wait for pending output to be sent, discard queued data (TCIFLUSH, TCOFLUSH, TCIOFLUSH), or suspend and resume flow (TCOOFF, TCOON, TCIOFF, TCION). Each returns 0, or -1 (same semantics as C tcdrain(3), tcflush(3), tcflow(3)).
Example
posix_write($fd, $frame, strlen($frame));
tcdrain($fd); // wait until it has left the UART
tcflush($fd, TCIFLUSH); // drop whatever arrived meanwhileSets both directions to $baud with TCSANOW. Returns 0, or -1. The one function here with no C counterpart, because C has no portable way to ask for a rate that isn't a B* constant.
- A rate with a
B*constant (50 through 4000000, where the platform defines it) goes throughtcgetattr,cfsetispeed,cfsetospeed,tcsetattr. - Any other rate on Linux goes through the kernel's
termios2withBOTHER. - Any other rate elsewhere returns
-1withposi_errno()set toEINVAL. $baudoutside1..4294967295throwsValueError.
Example
posi_set_baud_rate($fd, 250000); // DMX: fine on Linux, EINVAL on macOSSome ioctl() requests, I2C_RDWR and SPI_IOC_MESSAGE among them, take a struct that holds pointers to further buffers. A PHP string has no stable address the kernel can be handed, so posi allocates request memory and gives you its address as an int to pack('P', ...) into the struct. Every address is checked against the live allocations before any memory is touched, so a foreign, freed or out-of-range access throws ValueError instead of crashing. Buffers still allocated when the request ends are freed then. The registry guards posi's own access only: an address packed into a struct goes to the kernel unchecked, and struct layout (padding, pointer width) is yours to get right.
Allocates $size zeroed bytes and returns their address. $size below 1 throws ValueError. The memory counts against memory_limit.
Releases a buffer. 0 is a no-op, like free(NULL); an address not handed out by posi_mem_alloc(), or freed already, throws ValueError.
Copies $data into the buffer at $offset. A range past the end throws ValueError.
Copies $size bytes out of the buffer from $offset. A negative size or a range past the end throws ValueError.
Example — an I2C_RDWR transaction: write a register number, read one byte back
const I2C_RDWR = 0x0707;
const I2C_M_RD = 0x0001;
$register = posi_mem_alloc(1);
$reply = posi_mem_alloc(1);
posi_mem_write($register, "\x00");
// struct i2c_msg { u16 addr; u16 flags; u16 len; u8 *buf; }: 16 bytes on 64-bit Linux
$messages = posi_mem_alloc(32);
posi_mem_write($messages, pack('vvvxxP', 0x53, 0, 1, $register) . pack('vvvxxP', 0x53, I2C_M_RD, 1, $reply));
// struct i2c_rdwr_ioctl_data { struct i2c_msg *msgs; u32 nmsgs; }
if (ioctl($fd, I2C_RDWR, ['bytes' => pack('PVx4', $messages, 2)], $unused) < 0) {
throw new RuntimeException('I2C_RDWR: errno ' . posi_errno());
}
printf("DEVID 0x%02X\n", ord(posi_mem_read($reply, 1)));
posi_mem_free($messages);
posi_mem_free($reply);
posi_mem_free($register);Returns the errno saved by the last posi call that failed. A successful call leaves it alone. Compare it with the E* constants, which carry the platform's values.
Example
if (posix_open('/dev/i2c-9', O_RDWR) < 0) {
echo match (posi_errno()) {
ENOENT => "no such bus",
EACCES => "not permitted; check the group on the node",
default => "errno " . posi_errno(),
}, "\n";
}- ext/posix: ships with PHP and is required. posi is the other half of it: process identity and signals there, descriptors and devices here, no overlap.
- ext-epoll: Linux. Wait on many of posi's descriptors at once instead of
posix_ppoll()on one. Takes the raw intposix_open()returns, and hands back an epoll fd that onlyposix_close()can close. - ext-kqueue: macOS. The same job on Darwin, and more than descriptors: a kqueue also watches timers, signals, and the children
posix_waitpid()reaps. Its fd, too, closes only throughposix_close(). - ext/pcntl: ships with PHP.
pcntl_fork()makes the children thatposix_wait()andposix_waitpid()reap.
This codebase was built with LLM assistance under human direction. Prompts, reviews, and architectural decisions were owned by a professional software engineer — the model accelerates drafting and exploration; it does not replace judgment on APIs, dependency direction, or what ships.
This repository carries an Open Knowledge Format bundle at .okf/: Markdown with YAML frontmatter, readable by you and loadable by a coding agent. It records what the README doesn't: how the module is laid out, every ioctl() and fcntl() argument mode and why each check exists, the termios array shape and the BOTHER round trip, the native-buffer registry, how the stub becomes posi_arginfo.h, the 0.9 to 0.10 call map, and a log of every change that altered behaviour.
Start at .okf/index.md. AGENTS.md is the short rule sheet an agent reads before touching the C, the stub, or packaging; each rule points into the bundle for the reason behind it.
Point your agent at the repo and it has the same context the maintainer has. Point yourself at it when the README's answer is "it does X" and you want "because".
- POSIX.1-2024: the standard every call here comes from. Each function in the API table names its page. The man pages are the manual; this README only adds what PHP changes.
ioctl(2)andioctl_list(2)on Linux: what the third argument is for each request. The driver's own header is the final word.termios(3): the flag words and control characters the termios array carries.- Upgrading from 0.9: the Zephir
Posi\*statics andmicroscrap/posixhelpers mapped call by call onto 0.10. - PIE: the PHP Foundation's extension installer.
gen_stub.php: the php-src stub format.posi.stub.phpis the API declaration;posi_arginfo.his generated from it.- Open Knowledge Format: the spec the
.okf/bundle follows.
The suite is Pest v4 and needs no hardware: pipes, FIFOs, scratch files, ptys and child processes stand in for devices. It runs on Linux and macOS. Build the module in the clone, then point php at it:
git clone https://github.com/php-io-extensions/posi.git
cd posi
phpize && ./configure --enable-posi && make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)"
composer install
php -n -d extension="$PWD/modules/posi.so" vendor/bin/pestTests: 55 passed (173 assertions)
Duration: 0.16s-n skips your php.ini, so the suite tests the clone and not an installed posi.so; without it, a PHP that already loads posi warns Module "posi" is already loaded and keeps the installed one. Where ext/posix is a shared extension rather than built in, as on Debian packaging, add -d extension=posix before the posi one, since -n dropped the ini that loads it. A module only loads in the PHP whose phpize built it: for Homebrew's ZTS build, run phpize and pass --with-php-config from /opt/homebrew/opt/php@8.4-zts/bin.
Open an issue first for anything beyond a typo. Pull requests change posi.stub.php and posi.c (or posi_termios.c) together, regenerate posi_arginfo.h with gen_stub, and add a Pest test for the behaviour. CI must be green on every cell of the matrix. Read AGENTS.md for the rules the code follows.
posi gives PHP code the same access to devices and memory as C code in the same process. See SECURITY.md for what that means and how to report a vulnerability.
MIT. See LICENSE.