Referencia Zigzkit (SDK consumido)

src/safety/fs.zig

Declaraciones públicas de src/safety/fs.zig (zkit (SDK consumido)).

ImplementadoSin versión del tren todavía· generada desde apps/docs/generated/zig/zkit.json

Página generada desde zkit@zkit-0.0.0-_fk2iAa4BQCqGFKJeeY9WvMQClX5kiZQ2iOZrVsxVyyN (src/root.zig). No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere.

zkit.safety.fs — abrir ficheros controlados por un cliente dentro de una raíz, sin path traversal, sin symlinks que escapen y sin TOCTOU.

El patrón clásico (el de styx/local_file.zig, que es el origen del path guard de aquí): rechazar .. léxicamente, realpath de la ruta y de la raíz, comprobar el prefijo con frontera, open(O_NOFOLLOW) y comparar (dev, ino) del fd con los del realpath. Cierra casi todo, pero sigue siendo RESOLUCIÓN POR RUTA: entre el realpath y el open un componente INTERMEDIO puede cambiarse por un symlink (O_NOFOLLOW sólo protege el último), y la comprobación de identidad llega después del open.

Root resuelve RELATIVO A UN DESCRIPTOR de la raíz, que se abre una vez (configuración del operador) y ya no depende de ninguna ruta:

  • Linux ≥ 5.6: openat2(root_fd, rel, RESOLVE_BENEATH | …). El kernel resuelve TODA la ruta atómicamente y garantiza que no sale de la raíz (ni por .., ni por symlink absoluto o relativo, ni por /proc magic-links). Sin ventana TOCTOU: es una sola syscall.
  • Resto (y Linux sin openat2 o bajo seccomp que lo bloquee): recorrido componente a componente con openat(dirfd, comp, O_NOFOLLOW | O_DIRECTORY). Cada paso se hace sobre el fd del anterior; un symlink en cualquier componente es error. Tampoco hay ventana: nunca se vuelve a resolver una ruta.

Política de symlinks (Symlinks):

  • .reject (defecto): ningún componente puede ser symlink.
  • .beneath: se siguen symlinks mientras el resultado quede dentro de la raíz (sólo con openat2; sin él cae a .reject, que es más estricto, nunca a menos).

Y además: require_regular (por defecto) rechaza directorios, FIFOs y dispositivos (error.NotRegularFile): un FIFO bajo la raíz bloquearía un lector para siempre.

Montajes (Mounts): por defecto (.same) una apertura no cruza ningún punto de montaje bajo la raíz, tampoco un bind mount de un fichero de fuera sobre un nombre de dentro (mismo dev, así que comparar dev no lo ve). openat2 lleva RESOLVE_NO_XDEV; además, en Linux, el mnt_id (statx) de lo abierto tiene que ser el de la raíz, lo que cubre también el recorrido, que por sí solo no distingue un montaje. .cross lo relaja para la raíz que lo necesite (bind mounts dentro de una biblioteca).

Entradas directas de la raíz (un servidor que gestiona su directorio de staging): deleteEntry, renameEntry, entries y syncDir. Sólo aceptan UN componente (validateEntryName: sin /, ni ./..), así que no hay ningún componente intermedio que resolver ni que cambiar por un symlink; y unlinkat/renameat actúan sobre el enlace, nunca lo siguen: un symlink plantado con ese nombre se borra o se reemplaza, lo que apunta queda intacto.

safety.fs.PathError

const · línea 57

pub const PathError = error{ /// Ruta vacía. EmptyPath, /// Ruta absoluta donde se exige relativa a la raíz. AbsolutePath, /// Un componente `..`. ParentReference, /// Byte NUL embebido o ruta más l …

Sin ///.

safety.fs.validateRelative

fn · línea 72

pub fn validateRelative(path: []const u8) PathError!void

Validación LÉXICA de una ruta relativa a la raíz. Rechaza vacío, / inicial, componentes .. y NUL. . y // se toleran (no escapan).

safety.fs.relativeTo

fn · línea 89

pub fn relativeTo(root: []const u8, abs: []const u8) PathError![]const u8

Parte relativa de abs bajo root (ambas absolutas), con frontera de componente: /media/a NO contiene a /media/ab (el bug de prefijo que styx arregló en TKT-009). Léxico: no resuelve symlinks — eso lo hace Root. error.PathEscapesRoot si no está debajo.

safety.fs.EntryNameError

const · línea 101

pub const EntryNameError = PathError || error{ /// No es UNA entrada directa de la raíz: lleva `/`, o es `.`/`..`. NotAnEntryName, }

Sin ///.

safety.fs.validateEntryName

fn · línea 108

pub fn validateEntryName(name: []const u8) EntryNameError!void

Validación léxica de un nombre de entrada directa de la raíz: un solo componente, no vacío, sin /, sin NUL, distinto de . y ...

safety.fs.DeleteError

const · línea 115

pub const DeleteError = EntryNameError || error{ FileNotFound, AccessDenied, IsDir, FileBusy, ReadOnlyFileSystem, SystemResources, Unexpected, }

Sin ///.

safety.fs.RenameError

const · línea 125

pub const RenameError = EntryNameError || error{ FileNotFound, AccessDenied, IsDir, NotDir, DirNotEmpty, FileBusy, NoSpaceLeft, ReadOnlyFileSystem, SystemResources, Unexpected, }

Sin ///.

safety.fs.SyncDirError

const · línea 138

pub const SyncDirError = zfs.OpenError || zfs.WriteError

Sin ///.

safety.fs.EntriesError

const · línea 140

pub const EntriesError = zfs.OpenError || error{SystemResources}

Sin ///.

safety.fs.FileIdentity

type · línea 143

pub const FileIdentity = struct

Identidad de un objeto del sistema de ficheros (no de su ruta).

safety.fs.FileIdentity.ofFile

fn · línea 147

pub fn ofFile(f: zfs.File) zfs.StatError!FileIdentity

Sin ///.

safety.fs.FileIdentity.eql

fn · línea 152

pub fn eql(a: FileIdentity, b: FileIdentity) bool

Sin ///.

type · línea 157

pub const Symlinks = enum

Sin ///.

safety.fs.Resolver

type · línea 159

pub const Resolver = enum

Sin ///.

safety.fs.Mounts

type · línea 168

pub const Mounts = enum

¿Puede una apertura cruzar un punto de montaje bajo la raíz?

safety.fs.Access

type · línea 176

pub const Access = enum

Sin ///.

safety.fs.OpenOptions

type · línea 178

pub const OpenOptions = struct

Sin ///.

safety.fs.CreateOptions

type · línea 186

pub const CreateOptions = struct

Sin ///.

safety.fs.Error

const · línea 196

pub const Error = zfs.OpenError || zfs.StatError || PathError || error{ /// Un componente es un symlink y la política lo prohíbe. SymlinkRejected, /// No es un fichero regular (directorio, FIFO, dis …

Sin ///.

safety.fs.Opened

type · línea 203

pub const Opened = struct

Sin ///.

safety.fs.Opened.identity

fn · línea 207

pub fn identity(o: Opened) FileIdentity

Sin ///.

safety.fs.Root

type · línea 212

pub const Root = struct

Sin ///.

safety.fs.Root.open

fn · línea 222

pub fn open(path: []const u8) Error!Root

Abre la raíz (ruta del OPERADOR, p. ej. STYX_MEDIA_ROOT). La raíz en sí puede ser un symlink (lo decide quien configura); lo que hay DEBAJO se rige por la política de cada apertura.

safety.fs.Root.OwnedError

const · línea 232

pub const OwnedError = Error || zfs.MakeDirError || error{ /// El directorio es de otro uid: quien lo creó antes que este /// proceso decide qué hay dentro (symlinks o ficheros plantados). NotOwned, …

Sin ///.

safety.fs.Root.OwnedOptions

type · línea 241

pub const OwnedOptions = struct

Sin ///.

safety.fs.Root.openOwned

fn · línea 258

pub fn openOwned(path: []const u8, opts: OwnedOptions) OwnedError!Root

Abre un directorio PRIVADO del proceso (estado, caché, staging: lo que nadie más debe leer ni escribir) como raíz, y lo crea 0700 si no existe. A diferencia de open, la ruta la fija el proceso y un directorio que ya estaba ahí no se acepta sin más: con un nombre predecible en un directorio compartido (/tmp/x), otro usuario lo crea antes y es suyo. Se exige:

  • el último componente no es un symlink (O_NOFOLLOW);
  • propietario = uid efectivo del proceso (NotOwned si no);
  • sin ningún permiso de grupo ni de otros (AccessibleByOthers). La comprobación es sobre el fd ya abierto, no sobre la ruta: lo que se valida es lo que se usará. Las entradas se crean después con createFile (O_EXCL, 0600 por defecto) y se borran con deleteEntry.

safety.fs.Root.close

fn · línea 280

pub fn close(self: *Root) void

Sin ///.

safety.fs.Root.openFile

fn · línea 286

pub fn openFile(self: *const Root, rel: []const u8, opts: OpenOptions) Error!Opened

Abre rel (relativa a la raíz) según opts.

safety.fs.Root.createFile

fn · línea 304

pub fn createFile(self: *const Root, rel: []const u8, opts: CreateOptions) Error!zfs.File

Crea rel bajo la raíz. Los directorios intermedios deben existir (y no ser symlinks). Con exclusive un symlink plantado en el nombre final hace fallar la creación (PathAlreadyExists), no la redirige.

safety.fs.Root.makeDir

fn · línea 312

pub fn makeDir(self: *const Root, rel: []const u8, opts: zfs.MakeDirOptions) (Error || zfs.MakeDirError)!void

Crea el directorio rel bajo la raíz (padres deben existir).

safety.fs.Root.deleteEntry

fn · línea 326

pub fn deleteEntry(self: *const Root, name: []const u8) DeleteError!void

Borra la entrada name de la raíz (unlinkat, relativo al fd). Un symlink con ese nombre se borra él, sin seguirlo; un directorio es error.IsDir (no borra árboles) en toda plataforma.

safety.fs.Root.renameEntry

fn · línea 341

pub fn renameEntry(self: *const Root, from: []const u8, to: []const u8) RenameError!void

Renombra la entrada from a to, las dos directas de la raíz (renameat, atómico). Si to existe se reemplaza — también un symlink plantado con ese nombre: se sustituye el enlace, lo que apuntaba no se toca. Un from symlink se renombra como enlace.

safety.fs.Root.syncDir

fn · línea 370

pub fn syncDir(self: *const Root) SyncDirError!void

fsync del directorio raíz: hace durables los createFile, renameEntry y deleteEntry ya hechos. El fd de la raíz es O_PATH en Linux (sólo búsqueda) y no se puede sincronizar: abre . debajo para lectura, sincroniza y cierra.

safety.fs.Root.SpaceError

const · línea 376

pub const SpaceError = error{ /// Sólo Linux de 64 bits: en otro sistema no hay `fstatfs` de este /// módulo (quien llama decide si eso es fallar cerrado). Unsupported, Unexpected, }

Sin ///.

safety.fs.Root.availableBytes

fn · línea 387

pub fn availableBytes(self: *const Root) SpaceError!u64

Bytes que un proceso sin privilegios puede escribir aún en el sistema de ficheros de la raíz (f_bavail × f_bsize de fstatfs sobre el fd de la raíz, el mismo que usan las aperturas: no se resuelve ninguna ruta). Satura en maxInt(u64).

safety.fs.Root.entries

fn · línea 400

pub fn entries(self: *const Root) EntriesError!Entries

Recorre las entradas directas de la raíz (sin . ni ..). Cierra el iterador con close. Borrar con deleteEntry la entrada que se acaba de recibir es seguro; si una entrada creada o borrada por otro durante el recorrido aparece o no, POSIX no lo fija.

safety.fs.Root.openAbsolute

fn · línea 411

pub fn openAbsolute(self: *const Root, root_path: []const u8, abs: []const u8, opts: OpenOptions) Error!Opened

Atajo: abs debe estar léxicamente bajo root_path (la misma ruta con la que se abrió la raíz), y se abre su parte relativa.

safety.fs.Entries

type · línea 482

pub const Entries = struct

Iterador de Root.entries.

safety.fs.Entries.next

fn · línea 489

pub fn next(self: *Entries) ?[]const u8

Nombre de la siguiente entrada, o null al acabar (o si el sistema falla leyendo: no hay forma de distinguirlo sin errno, y para quien barre un directorio es lo mismo). El slice vale hasta el siguiente next o close.

safety.fs.Entries.close

fn · línea 498

pub fn close(self: *Entries) void

Sin ///.