← Sistemas de Archivos y E/SBloque 8 · Permisos y protección · Tema 26 de 50

Tema 26

Propietario, grupo y otros

Los nueve bits rwx no se suman entre todas las personas relacionadas con un archivo: se selecciona una clase según las credenciales del proceso. Aprenderemos a leer modos simbólicos y octales, distinguiremos lectura, escritura y ejecución en archivos y directorios, y separaremos este modelo de la decisión completa del kernel. Dos programas en C, un laboratorio con archivos propios y dos simuladores permitirán comprobar cada paso.

1. Atributos del objeto y credenciales del proceso

Los temas anteriores explicaron cómo una ruta llega a un objeto y cómo sus nombres pueden cambiar. Comenzamos ahora el bloque de protección: localizar un archivo no significa que cualquier proceso pueda leerlo, modificarlo o ejecutarlo.

Permisos clásicos: nueve bits asociados al objeto que describen lectura, escritura y ejecución o búsqueda para tres clases: propietario, grupo y otros. Su interpretación exige conocer también las credenciales del proceso y la operación solicitada.

dos lados de la comprobación · datos distintos
Objeto:
    UID propietario = 1000
    GID del archivo = 2000
    modo = 0640

Proceso:
    identidad usada para acceder al sistema de archivos
    grupo usado para esa comprobación
    lista de grupos suplementarios

Operación:
    leer, escribir, ejecutar, buscar un nombre, crear una entrada...

El propietario es un UID y el grupo del objeto es un GID. Los nombres que muestra ls, como ana o equipo, son una presentación obtenida mediante los servicios de cuentas del entorno. El kernel no necesita comparar literalmente esas cadenas para seleccionar una clase.

Un archivo tiene un grupo propietario, pero un proceso puede pertenecer a varios grupos. Los grupos suplementarios importan al comprobar si corresponde la clase de grupo. Que un usuario haya sido incorporado a un grupo en la base de cuentas no obliga a que un proceso ya iniciado haya renovado sus credenciales.

Los atributos se consultan con stat o variantes y las credenciales con llamadas como getuid, geteuid, getgid, getegid y getgroups. La referencia inode(7) describe los campos del objeto; credentials(7) describe las identidades del proceso.

2. Las tres clases de permisos

ClaseLetra habitualCuándo se selecciona en el modelo clásico
PropietariouLa identidad de acceso coincide con el UID propietario.
GrupogNo es el propietario y el GID del objeto coincide con el grupo de acceso o alguno suplementario.
OtrosoNo corresponde ninguna de las dos clases anteriores.

Otros no es un grupo concreto de la base de cuentas: es la clase restante de esa comprobación. La palabra grupo en el modo se refiere al GID del archivo, no a cualquiera de los grupos existentes en el sistema.

En este tema usaremos procesos sin privilegios especiales y objetos sin ACL extendida para explicar la selección básica. Esa declaración permite razonar con precisión; después señalaremos qué otras condiciones pueden intervenir en una operación real.

La clase se selecciona para cada objeto consultado. Un proceso puede ser propietario de un archivo pero entrar por la clase de grupo en uno de los directorios de su ruta. No existe una clase única que se aplique automáticamente a todo el recorrido.

3. Leer los nueve bits rwx

lectura del modo simbólico · archivo regular con modo 0640
-rw-r-----
│└─┬┘└─┬┘└─┬┘
│  │   │   └─ otros:      ---
│  │   └───── grupo:     r--
│  └───────── propietario: rw-
└──────────── tipo de objeto: archivo regular

Los nueve permisos son rw- r-- ---.
La primera posición no es otro bit de permiso.

Dentro de cada terna, r habilita lectura, w escritura y x ejecución o búsqueda según el tipo. Un guion indica ausencia de ese bit. Las letras aparecen en posiciones fijas; rw- no es lo mismo que r-x.

El primer carácter de ls -l expresa el tipo: por ejemplo, - para regular, d para directorio y l para enlace simbólico. Los bits de tipo y los permisos viven dentro de st_mode, pero se interpretan mediante máscaras distintas. Para comprobar tipo se usan macros como S_ISREG y S_ISDIR.

En sistemas reales pueden aparecer s, S, t o T en posiciones de ejecución, y un indicador adicional de ACL u otro contexto. No son errores de impresión: se relacionan con bits especiales y atributos que estudiaremos en el tema 28. El decodificador básico de este tema muestra solo rwx y declara ese alcance.

terminal · consultar sin modificar permisos
$ id
$ ls -ld -- .
$ stat -c 'nombre=%n tipo=%F uid=%u gid=%g modo=%a simbolico=%A' -- .

Los enlaces duros del mismo archivo comparten estos atributos. En Linux, los permisos ordinarios mostrados sobre el objeto enlace simbólico no conceden acceso a su destino, como vimos en el tema 24.

4. Representación octal

Cada terna usa tres bits: r vale 4, w vale 2 y x vale 1. Su suma forma una cifra octal de cero a siete. Tres cifras representan propietario, grupo y otros, en ese orden.

CifraBitsTernaCapacidades de esa clase
0000---Ninguna de las tres.
1001--xEjecución o búsqueda.
2010-w-Escritura.
3011-wxEscritura y ejecución o búsqueda.
4100r--Lectura.
5101r-xLectura y ejecución o búsqueda.
6110rw-Lectura y escritura.
7111rwxLas tres capacidades.
ejemplos · la cifra no cambia de significado al cambiar de clase
0600 → rw- --- ---
0640 → rw- r-- ---
0644 → rw- r-- r--
0750 → rwx r-x ---
0755 → rwx r-x r-x
0004 → --- --- r--

En C: 0640 es octal; 640 sin cero inicial es decimal.
Las máscaras S_IRUSR, S_IWUSR, etc. evitan depender de cifras al nombrar bits.

El cero inicial del literal de C no representa otra clase de permisos. Un modo presentado con cuatro cifras puede incluir en su primera cifra los bits especiales; este tema se centra en las tres cifras bajas. Tampoco se suman cifras de clases distintas para calcular lo que recibe un proceso.

5. Seleccionar una clase, sin mezclarla con otras

El error más frecuente consiste en buscar cualquier r, w o x disponible entre las nueve posiciones. En el modelo clásico se selecciona primero una sola clase y después se comprueba si contiene todos los bits requeridos.

algoritmo básico · sin ACL extendida ni privilegios especiales
si UID_de_acceso == UID_del_archivo:
    permisos = terna de propietario
si no, si GID_del_archivo está entre los grupos de acceso del proceso:
    permisos = terna de grupo
si no:
    permisos = terna de otros

permitir si (permisos & solicitados) == solicitados
denegar en caso contrario

No se vuelve a probar grupo u otros porque el propietario tenga pocos permisos. Tampoco se usa otros como alternativa cuando el proceso pertenece al grupo del archivo y la terna de grupo no autoriza la operación.

SituaciónModoClase seleccionadaLectura
El proceso es propietario.0004Propietario: ---Denegada, aunque otros tenga r.
No es propietario; pertenece al grupo.0604Grupo: ---Denegada, aunque otros tenga r.
No coincide UID ni grupo.0640Otros: ---Denegada.
No es propietario; coincide un grupo suplementario.0640Grupo: r--Permitida por estos bits.

Una solicitud combinada rw exige ambos bits dentro de la clase elegida. No basta con que propietario aporte r y grupo aporte w. En una apertura O_RDWR hacen falta las condiciones de lectura y escritura correspondientes, además del resto de comprobaciones de la operación.

Si el propietario denegado puede cambiar el modo en su contexto, podría modificar después la situación; eso es otra operación. Ser propietario no equivale a que una lectura actual ignore sus bits de acceso.

6. Qué significan rwx en un archivo regular

Un archivo de datos puede tener x y seguir sin un formato que el sistema pueda ejecutar. Un script requiere también las condiciones de su intérprete y del acceso a su texto. Un montaje noexec puede impedir ejecución aunque el modo muestre x.

El permiso w del archivo no equivale a permiso para borrar su nombre. Retirar una entrada modifica su directorio padre. Del mismo modo, cambiar el modo o la propiedad tiene sus propias reglas; no son una consecuencia automática de tener escritura del contenido.

El modo de apertura también importa. Un descriptor obtenido con O_RDONLY no se transforma en descriptor de escritura porque los bits del objeto permitan w. La autorización del objeto y las capacidades con las que se abrió el descriptor son conceptos relacionados pero distintos.

Las comprobaciones de acceso ordinarias se realizan al abrir y en otros puntos definidos por cada operación. Cambiar el modo después no revoca de forma general un descriptor regular ya abierto; tampoco garantiza que todas sus operaciones futuras vayan a tener éxito bajo cualquier política o estado del sistema.

7. Qué significan rwx en un directorio

El tipo directorio cambia la interpretación práctica: r permite enumerar nombres, x permite buscar y atravesar, y w interviene en modificar sus entradas. Para crear o retirar entradas normalmente se necesitan w y x en el padre, además de las demás condiciones.

Terna elegidaPosibilidad básicaLimitación
r--Leer nombres del directorio.No concede búsqueda para consultar los objetos por esos nombres.
--xBuscar un nombre conocido y atravesar.No concede enumerar la lista de nombres.
-wxModificar entradas cuyos nombres se conocen, si lo demás lo permite.No concede leer la lista completa.
r-xEnumerar y buscar.No concede crear o retirar entradas.
rw-Lectura de nombres y bit de escritura presente.La falta de búsqueda impide las modificaciones normales por nombre.

Con r sin x, una enumeración de nombres puede funcionar pero un listado que intente mostrar atributos de cada hijo puede fallar. Con x sin r puede abrirse un hijo conocido si tiene los permisos necesarios, aunque listar el directorio no esté permitido.

La herramienta puede solicitar más de lo mínimo necesario para una tarea abstracta. Por ejemplo, un programa que abra explícitamente el directorio con O_RDONLY necesitará condiciones para leerlo, aunque otra llamada pudiera buscar un hijo por ruta sin enumerarlo.

El bit x de un directorio no significa ejecutar sus nombres como programas. Es búsqueda. Esta distinción explica por qué un archivo legible puede resultar inaccesible si uno de sus directorios intermedios no permite atravesarlo.

8. Resolver rutas, crear y eliminar entradas

ruta · la comprobación del archivo no sustituye a la de sus padres
/proyecto/datos/informe.txt
    │       │        │
    │       │        └─ lectura: r en la clase del archivo
    │       └────────── búsqueda: x en la clase de datos
    └────────────────── búsqueda: x en la clase de proyecto

También deben poder atravesarse los demás directorios anteriores.
OperaciónRequisitos básicos relevantesNo exige por ese motivo
Leer archivo por nombrex en directorios atravesados y r en el archivo.Enumerar todos los padres.
Crear archivo nuevoAcceso al prefijo y w+x en el padre.Un permiso previo del archivo inexistente.
Retirar un nombre de archivoAcceso al prefijo y w+x en el padre, sujeto a reglas adicionales.w del contenido del archivo.
Renombrar entre padresCondiciones de ambos padres y de los objetos involucrados.Copiar todo el contenido.

Un usuario puede no tener escritura sobre un archivo y sí poder retirar su nombre en un directorio que controla, si no interviene una restricción adicional. Sticky bit, atributos, solo lectura y políticas de seguridad se estudiarán o recordarán según su alcance; no se deben omitir al interpretar un fallo real.

La referencia de resolución de rutas explica las comprobaciones de búsqueda. Que r del archivo sea suficiente en un modelo de un solo objeto no autoriza a ignorar el recorrido necesario para llegar hasta él.

9. Credenciales reales y límites del modelo

Un proceso tiene UID y GID reales y efectivos, además de identidades y capacidades adicionales. En Linux, el acceso ordinario al sistema de archivos utiliza fsuid y fsgid, que normalmente siguen a los efectivos, junto con los grupos suplementarios. No es correcto usar siempre el UID real como si fuera el que decide cada apertura.

En nuestros programas no se cambian credenciales ni se añaden capacidades. El inspector muestra identificadores reales y efectivos y la lista suplementaria, pero no pretende demostrar por sí solo el estado completo de seguridad del proceso.

La referencia de capabilities(7) detalla privilegios específicos de Linux. Los casos del laboratorio que esperan una denegación están dirigidos a una cuenta ordinaria sin privilegios ni ACL extendida que altere el resultado.

access plantea una consulta con credenciales y contrato diferentes de una apertura ordinaria. Tampoco evita que el árbol cambie antes de usarlo. En este tema no se usa para autorizar una apertura: se intenta la operación y se comprueba el resultado. El tema 29 estudiará access y la carrera TOCTOU.

10. Programa en C: observar atributos y credenciales

El inspector consulta una ruta con lstat y muestra identificadores numéricos, modo y grupos del proceso. No cambia permisos ni pretende calcular una autorización completa. Si la ruta final es un simbólico sin barra final, describe el propio enlace.

ver-permisos.c · separar la información del proceso de la del objeto
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s RUTA\n", argv[0]);
        return 1;
    }
    struct stat s;
    if (lstat(argv[1], &s) == -1) { perror("lstat"); return 1; }
    int n = getgroups(0, NULL);
    if (n == -1) { perror("getgroups cantidad"); return 1; }
    if ((uintmax_t)n > SIZE_MAX / sizeof(gid_t)) {
        fputs("Lista de grupos no representable\n", stderr);
        return 1;
    }
    gid_t *grupos = n ? malloc((size_t)n * sizeof *grupos) : NULL;
    if (n && !grupos) { perror("malloc grupos"); return 1; }
    int obtenidos = n ? getgroups(n, grupos) : 0;
    if (obtenidos == -1) { perror("getgroups lista"); free(grupos); return 1; }
    printf("proceso uid_real=%ju uid_efectivo=%ju gid_real=%ju gid_efectivo=%ju\n",
           (uintmax_t)getuid(), (uintmax_t)geteuid(),
           (uintmax_t)getgid(), (uintmax_t)getegid());
    fputs("grupos_suplementarios=", stdout);
    if (!obtenidos) fputs("(ninguno)", stdout);
    for (int i = 0; i < obtenidos; ++i)
        printf("%s%ju", i ? "," : "", (uintmax_t)grupos[i]);
    putchar('\n');
    free(grupos);

    const mode_t mascara[9] = {
        S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP,
        S_IROTH, S_IWOTH, S_IXOTH
    };
    const char *letras = "rwxrwxrwx";
    char basicos[10];
    for (int i = 0; i < 9; ++i)
        basicos[i] = s.st_mode & mascara[i] ? letras[i] : '-';
    basicos[9] = '\0';
    const char *tipo = S_ISREG(s.st_mode) ? "regular" :
                       S_ISDIR(s.st_mode) ? "directorio" :
                       S_ISLNK(s.st_mode) ? "enlace" : "otro";
    printf("objeto tipo=%s uid=%ju gid=%ju modo=%04o basicos=%s\n",
           tipo, (uintmax_t)s.st_uid, (uintmax_t)s.st_gid,
           (unsigned)(s.st_mode & 07777), basicos);
    puts("alcance=atributos_y_credenciales_no_autorizacion_completa");
    if (fflush(stdout) == EOF) { perror("fflush stdout"); return 1; }
    return 0;
}
terminal · compilar y consultar el directorio actual
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ver-permisos.c -o ver-permisos
$ ./ver-permisos .

El modo octal conserva los bits especiales bajos para no ocultar que podrían existir, pero basicos solo decodifica los nueve bits rwx. Por eso no reproduce las letras s o t de ls. Los identificadores se muestran como números sin consultar nombres de cuentas.

Getgroups no garantiza que la lista incluya el GID efectivo, por lo que este se muestra aparte. El programa no cambia grupos ni ejecuta operaciones concurrentes que los cambien entre sus consultas; si la segunda consulta falla, lo informa. La referencia de getgroups(2) describe esa interfaz.

11. Programa en C: evaluar el modelo clásico

El evaluador siguiente trabaja con identificadores hipotéticos recibidos como texto. No cambia UID, GID ni grupos del proceso y no intenta abrir ningún archivo. Calcula únicamente la selección de clase y los bits solicitados.

Recibe modo, UID y GID del objeto, UID y GID de acceso del proceso, lista suplementaria separada por comas o un guion si está vacía, y una petición no vacía formada por r, w y x sin repeticiones. Admite identificadores decimales de 0 a 4294967294. Incluso UID cero se trata como un número ordinario: este modelo no incluye privilegios.

evaluar-rwx.c · selección de una terna y comprobación de todos los bits
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

static int leer_id(const char *s, size_t n, uintmax_t *valor)
{
    const uintmax_t limite = UINT32_MAX - (uintmax_t)1;
    uintmax_t x = 0;
    if (n == 0) return -1;
    for (size_t i = 0; i < n; ++i) {
        if (s[i] < '0' || s[i] > '9') return -1;
        unsigned d = (unsigned)(s[i] - '0');
        if (x > (limite - d) / 10) return -1;
        x = x * 10 + d;
    }
    *valor = x;
    return 0;
}

static int leer_modo(const char *s, unsigned *modo)
{
    size_t n = strlen(s);
    if (n == 4 && s[0] == '0') { ++s; --n; }
    if (n != 3) return -1;
    unsigned m = 0;
    for (size_t i = 0; i < n; ++i) {
        if (s[i] < '0' || s[i] > '7') return -1;
        m = m * 8 + (unsigned)(s[i] - '0');
    }
    *modo = m;
    return 0;
}

static int en_grupos(const char *s, uintmax_t gid, int *pertenece)
{
    if (!strcmp(s, "-")) return 0;
    const char *p = s;
    for (;;) {
        const char *fin = strchr(p, ',');
        size_t n = fin ? (size_t)(fin - p) : strlen(p);
        uintmax_t grupo;
        if (leer_id(p, n, &grupo) == -1) return -1;
        if (grupo == gid) *pertenece = 1;
        if (!fin) return 0;
        p = fin + 1;
    }
}

int main(int argc, char **argv)
{
    if (argc != 8) {
        fprintf(stderr, "Uso: %s MODO UID_OBJ GID_OBJ UID_PROC GID_PROC GRUPOS PETICION\n", argv[0]);
        return 2;
    }
    unsigned modo, pedido = 0;
    uintmax_t uobj, gobj, uproc, gproc;
    if (leer_modo(argv[1], &modo) == -1 ||
        leer_id(argv[2], strlen(argv[2]), &uobj) == -1 ||
        leer_id(argv[3], strlen(argv[3]), &gobj) == -1 ||
        leer_id(argv[4], strlen(argv[4]), &uproc) == -1 ||
        leer_id(argv[5], strlen(argv[5]), &gproc) == -1) goto invalido;
    int grupo = gobj == gproc;
    if (en_grupos(argv[6], gobj, &grupo) == -1 || !argv[7][0]) goto invalido;
    for (const char *p = argv[7]; *p; ++p) {
        unsigned bit = *p == 'r' ? 4 : *p == 'w' ? 2 : *p == 'x' ? 1 : 0;
        if (!bit || (pedido & bit)) goto invalido;
        pedido |= bit;
    }
    unsigned bits;
    const char *clase;
    if (uproc == uobj) { clase = "propietario"; bits = (modo >> 6) & 7; }
    else if (grupo) { clase = "grupo"; bits = (modo >> 3) & 7; }
    else { clase = "otros"; bits = modo & 7; }
    char terna[4] = { bits & 4 ? 'r' : '-', bits & 2 ? 'w' : '-',
                      bits & 1 ? 'x' : '-', '\0' };
    int permite = (bits & pedido) == pedido;
    printf("clase=%s bits=%u terna=%s solicitud=%u resultado=%s\n",
           clase, bits, terna, pedido, permite ? "permitido" : "denegado");
    if (fflush(stdout) == EOF) { perror("fflush stdout"); return 2; }
    return permite ? 0 : 1;
invalido:
    fputs("Entrada inválida: modo, identificadores, grupos o petición\n", stderr);
    return 2;
}
terminal · tres decisiones con credenciales hipotéticas
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 evaluar-rwx.c -o evaluar-rwx
$ ./evaluar-rwx 004 1000 2000 1000 3000 - r
$ ./evaluar-rwx 604 1000 2000 1001 3000 2000 r
$ ./evaluar-rwx 640 1000 2000 1001 3000 2000 r

Los dos primeros devuelven 1 de manera esperada: no hay alternativa automática a otra terna. El tercero devuelve 0 por pertenencia suplementaria al grupo. El estado 2 indica entrada inválida o fallo de salida, distinto de una denegación calculada.

No se debe usar el resultado de este programa como autorización previa de una operación real. Omite permisos del prefijo, ACL, capacidades, atributos, estado del montaje y cambios concurrentes. Su propósito es hacer comprobable el algoritmo de los bits clásicos.

12. Laboratorio de decisiones y acceso real

Usa Linux o WSL con GCC y Python 3, una cuenta ordinaria sin capacidades especiales y un árbol de archivos propios. En WSL trabaja preferentemente dentro del directorio personal de Linux. No hace falta crear usuarios, cambiar propietarios ni ejecutar como administrador.

En este tema solo fijaremos modos de objetos nuevos del ensayo para observar sus efectos. El uso sistemático de chmod, chown y umask se desarrollará en el tema 27. No se modifican permisos de otros directorios ni políticas globales.

terminal · preparar un directorio nuevo y compilar los fuentes guardados
$ LAB26=$(mktemp -d "$HOME/tema26.XXXXXX")
$ cd -- "$LAB26"
$ id
$ findmnt -T .
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ver-permisos.c -o ver-permisos
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 evaluar-rwx.c -o evaluar-rwx

Comprobar la selección de clase sin cambiar las credenciales

El primer verificador usa valores hipotéticos. Incluye coincidencia por grupo primario y suplementario, prioridad del propietario, solicitudes combinadas e identidades distintas. No intenta hacerse pasar por esos usuarios en el sistema.

verificar-modelo-rwx.py · una referencia independiente por ternas octales
import subprocess

modos = ("000", "004", "040", "044", "400", "444", "604", "640",
         "0644", "0750", "755", "777")
peticiones = {"r": 4, "w": 2, "x": 1, "rw": 6, "rx": 5, "wx": 3, "rwx": 7}
escenarios = [
    (1000, 3000, "-", "propietario", 0),
    (1000, 2000, "2000", "propietario", 0),
    (1001, 2000, "-", "grupo", 1),
    (1001, 3000, "5000,2000", "grupo", 1),
    (1001, 3000, "5000", "otros", 2),
]
casos = 0
for modo in modos:
    ternas = [int(c, 8) for c in modo[-3:]]
    for uid, gid, grupos, clase, posicion in escenarios:
        for peticion, bits in peticiones.items():
            p = subprocess.run(["./evaluar-rwx", modo, "1000", "2000",
                                str(uid), str(gid), grupos, peticion],
                               capture_output=True, text=True)
            permite = all(ternas[posicion] & b for b in (4, 2, 1) if bits & b)
            assert p.returncode == (0 if permite else 1), p.stderr
            campos = dict(c.split("=", 1) for c in p.stdout.split())
            assert campos["clase"] == clase
            assert int(campos["bits"]) == ternas[posicion]
            assert campos["resultado"] == ("permitido" if permite else "denegado")
            casos += 1

validos = ["640", "1000", "2000", "1001", "3000", "-", "r"]
for indice, valor in [(0, "888"), (0, "1640"), (1, "-1"),
                       (1, "4294967295"), (5, "2000,"),
                       (5, "2000,,3000"), (6, "rr"), (6, "")]:
    argumentos = validos.copy()
    argumentos[indice] = valor
    p = subprocess.run(["./evaluar-rwx", *argumentos], capture_output=True)
    assert p.returncode == 2 and p.stderr and not p.stdout
print(f"{casos} decisiones correctas; entradas inválidas rechazadas")
terminal · ejecutar las pruebas del modelo
$ python3 verificar-modelo-rwx.py

Observar las decisiones reales sobre archivos propios

El segundo verificador realiza aperturas y operaciones de directorio. Detecta el caso de ejecución como root y termina con 77 para indicar que omite una prueba que requiere denegaciones sin privilegios. Si otra política o capacidad altera los resultados, conserva el fallo y describe el entorno; no lo resuelvas ampliando permisos ajenos.

verificar-acceso-real.py · propietario sin alternativa y r/x de directorio
from pathlib import Path
import errno
import os
import subprocess
import sys
import tempfile

if os.geteuid() == 0:
    print("Prueba omitida: usa una cuenta ordinaria sin privilegios", file=sys.stderr)
    raise SystemExit(77)

base = Path(tempfile.mkdtemp(prefix="prueba-permisos-", dir="."))
archivo = base / "dato.txt"
archivo.write_bytes(b"datos\n")

def abrir(ruta, flags, permitido):
    try:
        fd = os.open(ruta, flags | os.O_CLOEXEC)
    except OSError as e:
        assert not permitido and e.errno == errno.EACCES, e
    else:
        try:
            assert permitido, "La apertura debía ser denegada en este entorno"
        finally:
            os.close(fd)

for modo, lectura, escritura in [(0o004, False, False),
                                 (0o040, False, False),
                                 (0o400, True, False),
                                 (0o200, False, True),
                                 (0o600, True, True)]:
    os.chmod(archivo, modo)
    abrir(archivo, os.O_RDONLY, lectura)
    abrir(archivo, os.O_WRONLY, escritura)
    abrir(archivo, os.O_RDWR, lectura and escritura)

os.chmod(archivo, 0o600)
fd = os.open(archivo, os.O_RDWR | os.O_CLOEXEC)
try:
    os.chmod(archivo, 0)
    abrir(archivo, os.O_RDONLY, False)
    assert os.pread(fd, 6, 0) == b"datos\n"
finally:
    os.close(fd)
    os.chmod(archivo, 0o600)

d = base / "directorio"
d.mkdir()
hijo = d / "legible"
hijo.write_bytes(b"hijo\n")
os.chmod(hijo, 0o400)
try:
    os.chmod(d, 0o100)  # x sin r: buscar un nombre conocido
    assert hijo.read_bytes() == b"hijo\n"
    try:
        os.listdir(d)
    except PermissionError:
        pass
    else:
        raise AssertionError("La enumeración sin r debía fallar")

    os.chmod(d, 0o400)  # r sin x: nombres sin búsqueda
    assert "legible" in os.listdir(d)
    try:
        os.stat(hijo)
    except PermissionError:
        pass
    else:
        raise AssertionError("La consulta del hijo sin búsqueda debía fallar")

    os.chmod(d, 0o300)  # w+x sin r: entradas conocidas
    nuevo = d / "nuevo"
    f = os.open(nuevo, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    os.close(f)
    os.unlink(hijo)     # no exige w del contenido del hijo
finally:
    os.chmod(d, 0o700)

assert sorted(p.name for p in d.iterdir()) == ["nuevo"]
p = subprocess.run(["./ver-permisos", str(archivo)], capture_output=True, text=True)
assert p.returncode == 0, p.stderr
assert "modo=0600 basicos=rw-------" in p.stdout
print(p.stdout, end="")
print("Correctos: selección del propietario, aperturas, descriptor existente y r/w/x de directorio")
print("Árbol conservado:", base)
terminal · ejecutar con la cuenta ordinaria
$ python3 verificar-acceso-real.py

El archivo de modo 0004 pertenece al ejecutor: tener r para otros no le concede lectura porque se selecciona propietario. Las pruebas de grupo y otros se validan con identificadores hipotéticos en el primer programa; este laboratorio no cambia las credenciales reales para forzar esas clases.

El bloque finally restituye acceso al directorio del ensayo incluso si una comprobación intermedia falla. La prueba sobre el descriptor ya abierto demuestra el comportamiento ordinario observado en ese entorno, no una promesa de que permisos o políticas no puedan afectar nunca a ninguna operación posterior.

13. Simuladores de bits y operaciones

Seleccionar la clase y comprobar una petición

El primer modelo permite editar los nueve bits o aplicar un modo octal, introducir identificadores hipotéticos y elegir qué permisos se solicitan. Los cambios no afectan a ningún archivo ni a las credenciales reales del equipo.

Propietario, grupo u otros: una sola terna

Claserwx
Propietario
Grupo
Otros

comparación de bits de la clase elegida
ClaseTernaCifraUso en esta comprobación

Con los valores iniciales se selecciona grupo por pertenencia suplementaria, pero w se deniega. Cambia la solicitud a r y calcula: debe permitirse por los bits. Después prueba UID de proceso 1000 y modo 004 para observar que otros no sirve de alternativa al propietario.

Las casillas recalculan el modo; al editar los campos de texto o la solicitud, pulsa el botón para aplicar el escenario. Usa grupos separados por comas sin espacios o un guion para ninguno. El modelo no incluye permisos del prefijo, ACL, capacidades, atributos ni estado del montaje. UID cero tampoco recibe privilegios especiales en este cálculo hipotético.

La operación decide qué objetos importan

El segundo panel representa /proyecto/datos/informe.txt. Cada terna ya es la clase seleccionada para ese objeto: puede ser distinta en cada nivel. El resto del prefijo permite búsqueda. Para crear se usa un nombre nuevo dentro de datos y el modo del archivo mostrado no interviene.

Recorrido, objeto final y directorio padre

resultado bajo las condiciones representadas
ObjetoTerna elegidaBits requeridosResultado de este paso

Quita x de proyecto y observa que r del archivo no basta para abrirlo. Después permite x en proyecto, elige datos=r-- y enumera nombres: puede pasar sin búsqueda en datos. Consultar atributos del hijo conocido exige x en ese padre. Con datos=-wx puedes retirar el nombre aunque informe.txt no tenga w.

El montaje de solo lectura demuestra otra condición independiente: los bits pueden ser suficientes y la modificación seguir impedida. El panel comprueba primero sus máscaras para explicar los pasos; no promete el orden exacto de errores que devuelve cualquier operación real. No simula sticky, ACL, políticas de seguridad, permisos de directorios adicionales ni concurrencia.

14. Diagnosticar una denegación

  1. Identifica la operación. Leer contenido, abrir para escribir, enumerar, buscar, crear o retirar una entrada no exigen lo mismo.
  2. Identifica las credenciales del proceso real. UID/GID de acceso y grupos vigentes; no solo los nombres de la cuenta.
  3. Consulta los objetos relevantes. Archivo y directorios del prefijo, distinguiendo enlaces propios de destinos seguidos.
  4. Selecciona la clase en cada objeto. Propietario primero; después grupo; finalmente otros. No combines las ternas.
  5. Comprueba todos los bits necesarios. Una solicitud combinada debe satisfacerse completa en la terna elegida.
  6. Revisa condiciones adicionales. ACL, capacidades, montaje, atributos y política aplicable cuando el modelo básico no explica el resultado.
  7. Conserva el error efectivo. EACCES, EROFS, ENOENT y otros fallos no describen la misma causa.

Para presentar el recorrido, herramientas como namei -l pueden mostrar los componentes y atributos de una ruta. Su consulta también está sujeta al entorno visible. No se obtiene una instantánea completa de seguridad leyendo una sola fila de ls.

Un signo + junto al modo puede indicar una ACL que requiere otra interpretación. La máscara de ACL puede influir en los bits de grupo mostrados. No deduzcas los permisos efectivos de todas sus entradas sin consultar la ACL; el tema 28 desarrollará esa relación.

La propiedad del archivo puede permitir cambiar su modo bajo ciertas condiciones, pero eso no demuestra que la operación denegada ya estuviera autorizada. Diagnosticar y cambiar permisos son pasos diferentes; el tema 27 tratará los cambios con alcance concreto.

La información observada puede cambiar antes de la apertura. El código de aplicación debe comprobar el resultado de la operación real, como explica open(2), y no usar una predicción del modo como garantía futura.

15. Resumen

16. Preguntas de repaso y práctica

1. ¿Propietario y grupo se guardan como nombres de texto?

Los campos del objeto son identificadores numéricos UID y GID. Las herramientas pueden resolverlos a nombres mediante los servicios de cuentas del entorno.

2. ¿Otros es un grupo concreto de usuarios?

No. Es la clase restante para una comprobación cuando no se selecciona propietario ni grupo.

3. ¿Qué representa el primer carácter de -rw-r-----?

El tipo de objeto. Los nueve permisos comienzan después. En ese ejemplo el objeto es regular y el modo básico es 0640.

4. ¿Cómo se representa r-x en octal?

Con 5, porque r aporta 4 y x aporta 1. La terna -wx es 3 y rw- es 6.

5. ¿Por qué se escribe 0640 en un literal de C?

El cero inicial indica base octal. El literal 640 sin ese cero es decimal y no representa el mismo conjunto de bits.

6. ¿El propietario de un archivo 0004 puede leerlo por otros?

No en el modelo clásico sin privilegios ni ACL extendida. Se selecciona propietario, cuya terna no tiene lectura; no hay alternativa a otros.

7. ¿Un miembro del grupo puede usar otros si grupo le deniega?

No en ese modelo. Una vez seleccionada grupo se evalúa su terna, sin recurrir a otros.

8. ¿Importan los grupos suplementarios?

Sí. Si no se seleccionó propietario, la coincidencia del GID del objeto con alguno de ellos puede seleccionar grupo.

9. ¿Un archivo tiene tantos grupos como el proceso?

No. Tiene un GID propietario, aunque el proceso pueda pertenecer a varios grupos.

10. ¿Una solicitud rw puede combinar r de propietario con w de grupo?

No. Ambos bits deben estar presentes en la terna seleccionada para esa comprobación.

11. ¿x convierte cualquier archivo en un programa ejecutable?

No. Es una condición de autorización, no una conversión de formato. También importan formato, intérprete, montaje y otras políticas.

12. ¿r de un directorio permite necesariamente consultar todos sus hijos?

No. Permite enumerar nombres. Para buscar los hijos por nombre se necesita x en ese directorio y cumplir los demás requisitos.

13. ¿Puede abrirse un hijo conocido sin r de su padre?

Puede, si existe búsqueda x en el padre y el resto de la ruta y del objeto lo permite. No hace falta enumerar todos los nombres para buscar uno conocido.

14. ¿w del archivo autoriza a borrar su nombre?

No por sí solo. La eliminación modifica la entrada del directorio padre y requiere sus condiciones, además de restricciones como sticky cuando correspondan.

15. ¿Un archivo con todos los bits puede ser inaccesible por su ruta?

Sí. Un directorio anterior puede impedir búsqueda u otra política puede denegar la operación.

16. ¿Getgroups siempre incluye el GID efectivo?

No está garantizado. Se consulta y considera el GID efectivo por separado de la lista suplementaria.

17. ¿Las aperturas ordinarias usan siempre el UID real?

No. En Linux intervienen fsuid y fsgid, normalmente asociados a los efectivos, y los grupos suplementarios. Access tiene un contrato distinto.

18. ¿Cambiar el modo a cero revoca cualquier descriptor abierto?

No de forma general para un archivo regular ya abierto. El descriptor mantiene su modo de apertura, aunque otras condiciones pueden afectar operaciones posteriores.

19. ¿Los nueve bits explican toda denegación del kernel?

No. ACL, capacidades, estado del montaje, atributos y políticas adicionales pueden cambiar lo permitido.

20. ¿El evaluador del tema debe usarse para autorizar antes de abrir?

No. Solo calcula un modelo hipotético. La aplicación debe intentar la operación apropiada y comprobar su resultado, sin separar una predicción de un uso posterior como si fueran atómicos.

Para practicar

  1. Convierte 0600, 0640, 0644, 0710 y 0755 a ternas simbólicas y explica cada clase.
  2. Reconstruye el modo octal de rw- r-x --- y de r-- --- --x.
  3. Diseña un caso donde propietario tenga menos acceso que otros y explica por qué no se mezclan.
  4. Diseña otro caso donde un grupo suplementario seleccione la terna de grupo.
  5. Prueba solicitudes rw y rx y determina qué bit falta cuando se deniegan.
  6. Ejecuta el inspector sobre un archivo, un directorio y un simbólico y separa tipo de permisos.
  7. Ejecuta las 420 decisiones del verificador del modelo sin modificar las credenciales del equipo.
  8. Realiza el laboratorio real con una cuenta ordinaria y compara r sin x frente a x sin r en directorios propios.
  9. Explica por qué se pudo retirar el hijo legible de un padre -wx sin dar escritura al contenido del hijo.
  10. En el simulador de operaciones, permite los bits de escritura y activa montaje de solo lectura.
  11. Describe un diagnóstico que incluya identidades vigentes, prefijo, clase seleccionada y error efectivo.
  12. Explica qué condiciones quedarían fuera de una tabla que solo predijera acceso con st_mode.

En el tema 27 usaremos chmod, chown y umask desde Bash y C para cambiar permisos y propiedad y razonar sobre el modo con el que nacen los archivos.