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

Tema 27

chmod, chown y umask

Después de interpretar los permisos, toca cambiarlos de forma concreta y comprender cómo nacen. Usaremos modos octales y simbólicos, distinguiremos propiedad de acceso y calcularemos la máscara de creación sin recurrir a una resta engañosa. Dos programas en C, un laboratorio con objetos propios y dos simuladores mostrarán la diferencia entre pedir permisos al crear, filtrarlos con umask y modificar después el modo o el grupo.

1. Tres cambios con alcances diferentes

En el tema 26 seleccionamos propietario, grupo u otros para evaluar una petición. Ahora estudiaremos las operaciones que cambian esos atributos y la máscara que interviene al crear un objeto. No son mecanismos intercambiables.

MecanismoQué cambiaCuándo actúa
chmodBits del modo de un objeto existente.Al ejecutar la operación sobre ese objeto.
chown / chgrpUID o GID propietario, sujeto a autorización.Al cambiar la propiedad del objeto.
umaskMáscara de creación del proceso.En creaciones posteriores que usan ese mecanismo.

Modo solicitado: permisos que una aplicación pide al crear. Máscara de creación: bits que deben retirarse de esa petición en el caso sin ACL predeterminada. Modo observado: resultado real consultado después de la operación.

La máscara no modifica retrospectivamente todos los archivos del usuario. Tampoco convierte chmod en una creación: un fchmod posterior no se filtra con la umask como si fuera open con O_CREAT.

Los permisos pertenecen al objeto. Cambiar el modo o grupo de un archivo con varios enlaces duros se observa por todos esos nombres; no crea una política separada por cada ruta.

2. chmod con modo octal

Una forma numérica expresa el conjunto de bits deseado. En los ejemplos básicos trabajaremos con las tres cifras rwx de propietario, grupo y otros. Para los bits especiales y las reglas particulares sobre directorios habrá que considerar además el tema 28 y el contrato de la herramienta.

terminal · directorio de laboratorio nuevo y cambios de un archivo propio
$ LAB27=$(mktemp -d "$HOME/tema27.XXXXXX")
$ cd -- "$LAB27"
$ (set -o noclobber; printf 'datos\n' > nota.txt)
$ chmod 640 -- nota.txt
$ stat -c 'nombre=%n uid=%u gid=%g modo=%a simbolico=%A' -- nota.txt
$ chmod 600 -- nota.txt
$ stat -c 'modo=%a simbolico=%A' -- nota.txt

Para un archivo regular sin bits especiales, 0640 establece rw- r-- ---. Cambiar después a 0600 retira la lectura del grupo; no significa «sumar 600» al modo anterior.

Ser propietario puede autorizar a cambiar el modo aunque los bits actuales no concedan leer el contenido. Tener w del contenido no permite por sí solo cambiar el modo de un archivo ajeno. En Linux intervienen propiedad y capacidades específicas para esa modificación.

GNU chmod tiene reglas adicionales sobre conservación de setuid/setgid de directorios y formas numéricas para retirarlos explícitamente. No se debe generalizar de las tres cifras bajas a una afirmación de que cualquier comando numérico borra siempre todos los bits especiales.

3. chmod con modo simbólico

Una cláusula simbólica expresa a quién se aplica, qué operación realiza y qué permisos usa. Las cláusulas separadas por comas se aplican en orden.

estructura · clase, operador y permisos
u+r      propietario: añadir lectura
g-w      grupo: retirar escritura
o=       otros: dejar sin permisos básicos
u=rw,g=r,o=  establecer las tres ternas indicadas
a+r      todas las clases: añadir lectura
g=u      copiar la terna actual de propietario a grupo

u: propietario | g: grupo | o: otros | a: todas
+: añadir     | -: retirar | =: establecer
terminal · cambios acumulativos sobre nota.txt
$ chmod 640 -- nota.txt
$ chmod u+x,g+w -- nota.txt
$ stat -c 'modo=%a simbolico=%A' -- nota.txt
$ chmod g=u,o= -- nota.txt
$ stat -c 'modo=%a simbolico=%A' -- nota.txt

La primera modificación pasa de 0640 a 0760. Después, g=u copia rwx del propietario y o= deja otros vacío, resultando 0770. Copiar una terna usa los bits del objeto en esa fase, no una relación futura que siga sincronizando ambas clases.

Qué ocurre si se omite la clase

En GNU chmod, omitir u/g/o/a no equivale sin condiciones a escribir a. La umask influye en qué bits pueden verse afectados por esa forma. Para expresar una intención independiente de esa omisión, los ejemplos del curso nombran las clases.

terminal · comparar clase omitida y clase explícita dentro de un subshell
$ (umask 022; chmod 444 -- nota.txt; chmod +w -- nota.txt; stat -c 'sin_clase=%a' -- nota.txt)
$ (umask 022; chmod 444 -- nota.txt; chmod a+w -- nota.txt; stat -c 'con_a=%a' -- nota.txt)

Los resultados esperados son 0644 y 0666 respectivamente. Aquí la máscara afecta a la interpretación de la cláusula de la herramienta con clase omitida; no significa que la llamada al sistema chmod filtre de nuevo cualquier modo explícito.

La documentación de modos simbólicos de GNU describe sintaxis, copia y selección de clases. Nuestro simulador implementará solo un subconjunto declarado, con clases explícitas.

4. Directorios, X y alcance del cambio

Como vimos en el tema 26, x significa búsqueda en un directorio. Aplicar el mismo modo a todo un árbol puede retirar búsqueda de carpetas o añadir ejecución a documentos. Primero se debe distinguir el tipo y el alcance de la operación.

La X mayúscula de los modos simbólicos es condicional: pide ejecución/búsqueda si el objeto es directorio o si ya existe algún bit de ejecución. No es otro bit distinto de x; determina si la cláusula debe incluirlo.

terminal · comparar X sobre archivo regular y directorio
$ mkdir carpeta
$ chmod 640 -- nota.txt
$ chmod 640 -- carpeta
$ chmod a+X -- nota.txt carpeta
$ stat -c '%n modo=%a' -- nota.txt carpeta

Nota.txt permanece en 0640 porque no tenía ejecución. Carpeta pasa a 0751 al añadir x a las tres clases. Ese resultado no convierte la carpeta en un archivo ejecutable: le añade búsqueda.

La forma recursiva de una herramienta introduce decisiones sobre enlaces simbólicos, montajes, objetos ya accesibles y errores parciales. El laboratorio no necesita cambios recursivos: se trabaja con una lista explícita de objetos propios. Un fallo en un árbol no implica que se hayan deshecho los cambios anteriores.

5. chown, chgrp y autorización

Chown cambia propietario y, si se indica, grupo. Chgrp cambia el grupo. En Linux ordinario, cambiar el UID propietario requiere privilegios como CAP_CHOWN; ser propietario no autoriza a entregar libremente el archivo a cualquier UID.

El propietario puede cambiar el grupo de su archivo a uno de los grupos a los que pertenece, sujeto al sistema y sus políticas. Eso incluye el grupo de acceso pertinente y grupos suplementarios. El laboratorio solo utiliza archivos propios y grupos vigentes del ejecutor.

terminal · consultar identidades y fijar el grupo propio actual
$ id
$ stat -c 'antes uid=%u gid=%g modo=%a' -- nota.txt
$ chgrp -- "$(id -gn)" nota.txt
$ stat -c 'despues uid=%u gid=%g modo=%a' -- nota.txt

Si el archivo ya tenía ese GID, la prueba valida la operación sin mostrar un cambio numérico. Más adelante el programa C permitirá elegir otro GID de la lista propia si existe. No hace falta usar sudo ni cambiar usuarios para realizar el ensayo.

sintaxis de chown · ejemplos de forma, no órdenes necesarias del laboratorio
chown usuario archivo        cambiar UID propietario
chown usuario:grupo archivo  cambiar UID y GID
chown :grupo archivo         cambiar solo GID
chgrp grupo archivo          cambiar solo GID

Modificar UID/GID puede alterar quién selecciona cada terna y puede limpiar bits especiales o capacidades asociadas al archivo según el caso. Si se necesita un modo final concreto, la aplicación debe considerar el orden propiedad → modo y consultar el resultado. Los atributos especiales se desarrollarán en el tema 28.

El GID inicial de un objeto tampoco se deduce siempre solo del grupo efectivo: puede depender del directorio padre y de características como setgid o reglas de montaje. El nombre del directorio no cambia ese mecanismo por sí mismo.

6. umask filtra el modo solicitado

Sin ACL predeterminada que altere la creación, el resultado básico es la intersección entre lo pedido y los bits no retirados por la máscara. La máscara puede quitar permisos; no añade los que la aplicación no pidió.

fórmula · operaciones sobre bits, no resta aritmética
modo_resultante = modo_solicitado & (~umask & 0777)

solicitado 0666, máscara 0022 → 0644
solicitado 0666, máscara 0027 → 0640
solicitado 0777, máscara 0027 → 0750
solicitado 0600, máscara 0000 → 0600

Contraejemplo a la resta:
0666 & ~0033 → 0644
0666 - 0033  → 0633  (no representa el filtrado)
MáscaraRegular pedido con 0666Directorio pedido con 0777Lectura de la política básica
000006660777No retira bits de estas peticiones.
000206640775Retira escritura de otros.
002206440755Retira escritura de grupo y otros.
002706400750Retira escritura de grupo y todo de otros.
007706000700Retira todos los permisos de grupo y otros.

Las peticiones 0666 y 0777 son convenciones habituales de herramientas y bibliotecas, no una regla de que todo archivo deba pedirlas. Un programa puede pedir 0600 o 0700, y una máscara permisiva no lo amplía. Touch tampoco reinicializa el modo de un archivo ya existente.

Si el padre tiene una ACL predeterminada, Linux deriva la ACL de acceso de esa herencia y limita con el modo solicitado en vez de aplicar simplemente la fórmula de umask. El tema 28 explicará ese mecanismo. Los cálculos y el laboratorio de aquí declaran la ausencia de esa ACL.

La referencia de umask(2) especifica filtrado, herencia y la excepción de ACL. No hay que interpretar una cifra de máscara como si fuera directamente el modo que recibirán todos los objetos.

7. La máscara pertenece al contexto del proceso

Un hijo hereda la máscara y exec la conserva. Cambiarla en un shell afecta a sus creaciones posteriores y a los hijos que lance, no al shell padre desde el que se inició. Un subshell permite acotar una prueba sin modificar el resto de la sesión interactiva.

terminal · crear bajo una máscara local al subshell
$ umask
$ (umask 027; touch creado-027.txt; mkdir creado-027)
$ stat -c '%n modo=%a' -- creado-027.txt creado-027
$ umask

Si los nombres son nuevos y no hay ACL predeterminada, se esperan 0640 y 0750. La segunda consulta de umask fuera de los paréntesis debe conservar el valor original de la sesión. Dentro del subshell no se crean permisos de ejecución en el regular porque touch no los solicitó.

La llamada C umask devuelve el valor anterior a la vez que instala uno nuevo. El patrón de poner cero para «leer» y restaurar después modifica temporalmente el contexto: no es una consulta atómica. En programas multihilo puede afectar a creaciones concurrentes.

En Linux, el campo Umask de /proc/self/status permite observar la máscara del proceso lector sin cambiarla. Si ejecutas cat /proc/self/status, self identifica a cat, que normalmente heredó el valor del shell; no es una lectura directa de los atributos internos del shell por arte del nombre self.

Los ejemplos C siguientes son de un hilo y restauran la máscara anterior al terminar. Una biblioteca reutilizable debe evitar imponer cambios globales inesperados al contexto compartido de sus llamadores.

8. Interfaces desde C y referencias por descriptor

interfaces · cambiar un objeto existente y filtrar creaciones futuras
#include <sys/stat.h>
#include <unistd.h>

int chmod(const char *ruta, mode_t modo);
int fchmod(int fd, mode_t modo);

int chown(const char *ruta, uid_t uid, gid_t gid);
int fchown(int fd, uid_t uid, gid_t gid);

mode_t umask(mode_t mascara);    /* devuelve máscara anterior */

fchown(fd, (uid_t)-1, grupo);   /* conservar UID; solicitar ese GID */

Chmod y chown siguen un enlace simbólico final en sus formas ordinarias por ruta. Fchmod y fchown actúan sobre el objeto al que ya refiere el descriptor. Lchown y fchownat con opciones específicas permiten tratar el enlace mismo; no se trasladan sus reglas automáticamente al cambio de modo de enlaces en Linux.

Usar un descriptor estabiliza la referencia al objeto que se abrió. No reemplaza la autorización para modificarlo ni congela todo el árbol. Abrir de forma inadecuada una ruta hostil y luego usar fchmod no corrige por sí solo una resolución inicial equivocada.

En chown/fchown el valor de tipo uid_t o gid_t igual a -1 indica «no cambiar ese campo». No es una petición de asignar el número de identidad reservado que representaría todos los bits a uno.

Las referencias de chmod(2) y chown(2) detallan requisitos, variantes y efectos. Todas las operaciones del laboratorio comprueban su retorno y consultan el estado final.

9. Programa en C: crear con distintas condiciones

El primer programa recibe un directorio nuevo y una máscara octal. Crea un regular con petición 0666, otro con 0600 y un directorio con 0777. Consulta los modos y contrasta la fórmula. Después instala máscara 0777 y aplica fchmod(0640) al primer archivo para demostrar que esa modificación explícita no se filtra como una creación.

El directorio de ensayo se crea primero con petición 0700 bajo una máscara local 0077. Es una decisión independiente de la máscara que se quiere experimentar. El programa es de un hilo, conserva la máscara que heredó y la restaura en todas sus salidas posteriores.

crear-con-mascara.c · contrastar petición, máscara y modo observado
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

static int cerrar(int *fd)
{
    if (*fd == -1) return 0;
    int copia = *fd;
    *fd = -1;
    return close(copia);
}

static int octal(const char *s, mode_t *valor)
{
    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');
    }
    *valor = (mode_t)m;
    return 0;
}

static int foto(const char *fase, const struct stat *s,
                 mode_t pedido, mode_t mascara, mode_t esperado)
{
    mode_t real = s->st_mode & 0777;
    printf("%s pedido=%03o mascara=%03o real=%03o\n", fase,
           (unsigned)pedido, (unsigned)mascara, (unsigned)real);
    if (real != esperado) {
        fprintf(stderr, "Modo distinto de %03o: revisar ACL predeterminada y entorno\n",
                (unsigned)esperado);
        errno = EIO;
        return -1;
    }
    return 0;
}

int main(int argc, char **argv)
{
    mode_t mascara;
    if (argc != 3 || octal(argv[2], &mascara) == -1) {
        fprintf(stderr, "Uso: %s DIRECTORIO_NUEVO MASCARA_OCTAL\n", argv[0]);
        return 1;
    }
    int dir = -1, archivo = -1, privado = -1, estado = 1;
    struct stat s;
    mode_t anterior = umask(0077);
    if (mkdir(argv[1], 0700) == -1) goto error;
    dir = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir == -1) goto error;
    umask(mascara);
    archivo = openat(dir, "archivo", O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0666);
    if (archivo == -1 || fstat(archivo, &s) == -1 ||
        foto("archivo", &s, 0666, mascara, 0666 & ~mascara) == -1) goto error;
    privado = openat(dir, "privado", O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
    if (privado == -1 || fstat(privado, &s) == -1 ||
        foto("privado", &s, 0600, mascara, 0600 & ~mascara) == -1) goto error;
    if (mkdirat(dir, "carpeta", 0777) == -1 ||
        fstatat(dir, "carpeta", &s, AT_SYMLINK_NOFOLLOW) == -1 ||
        foto("carpeta", &s, 0777, mascara, 0777 & ~mascara) == -1) goto error;
    umask(0777);
    if (fchmod(archivo, 0640) == -1 || fstat(archivo, &s) == -1 ||
        foto("fchmod", &s, 0640, 0777, 0640) == -1) goto error;
    puts("creacion=correcta fchmod_no_filtrado=correcto");
    estado = 0;
    goto fin;
error:
    perror("operacion del ensayo");
fin:
    umask(anterior);
    if (cerrar(&privado) == -1) { perror("close privado"); estado = 1; }
    if (cerrar(&archivo) == -1) { perror("close archivo"); estado = 1; }
    if (cerrar(&dir) == -1) { perror("close dir"); estado = 1; }
    if (fflush(stdout) == EOF) { perror("fflush stdout"); estado = 1; }
    return estado;
}
terminal · compilar y experimentar con una máscara
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 crear-con-mascara.c -o crear-con-mascara
$ ./crear-con-mascara ensayo-027 027

Se esperan 0640, 0600 y 0750 durante las creaciones. La foto final vuelve a mostrar 0640 incluso bajo máscara 0777. Los archivos permanecen para inspección; una repetición sobre ensayo-027 se rechaza al crear el directorio.

Con máscaras muy restrictivas puede quedar una carpeta del ensayo sin búsqueda. El programa consulta sus metadatos desde el padre y no necesita abrir su contenido. Para inspeccionarla después, el propietario puede ajustar ese objeto propio según la práctica; no se ha retirado búsqueda del directorio raíz privado que lo contiene.

10. Programa en C: cambiar grupo y modo de un objeto propio

El segundo programa crea otro directorio nuevo y un archivo con dos nombres duros. El GID pedido debe pertenecer a los grupos vigentes del proceso; por defecto usa el efectivo. Cambia solo el GID mediante fchown, conserva el UID, aplica después fchmod(0640) y comprueba que el alias muestra el mismo resultado.

La validación de pertenencia es una restricción deliberada del ejemplo, incluso si se ejecutara con privilegios. No permite solicitar un UID ajeno y no modifica grupos del proceso ni la base de cuentas. El resultado real de fchown sigue comprobándose: la validación previa no sustituye a la autorización del sistema.

ajustar-propio.c · grupo permitido, modo final e identidad compartida
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

static int cerrar(int *fd)
{
    if (*fd == -1) return 0;
    int copia = *fd;
    *fd = -1;
    return close(copia);
}

static int leer_gid(const char *s, gid_t *gid)
{
    if (!*s) return -1;
    for (const char *p = s; *p; ++p)
        if (*p < '0' || *p > '9') return -1;
    errno = 0;
    char *fin;
    uintmax_t n = strtoumax(s, &fin, 10);
    if (errno || *fin || (uintmax_t)(gid_t)n != n || (gid_t)n == (gid_t)-1)
        return -1;
    *gid = (gid_t)n;
    return 0;
}

static int pertenece(gid_t gid)
{
    if (gid == getegid()) return 1;
    int n = getgroups(0, NULL);
    if (n == -1) return -1;
    if ((uintmax_t)n > SIZE_MAX / sizeof(gid_t)) { errno = EOVERFLOW; return -1; }
    gid_t *lista = n ? malloc((size_t)n * sizeof *lista) : NULL;
    if (n && !lista) return -1;
    int obtenidos = n ? getgroups(n, lista) : 0;
    if (obtenidos == -1) { int e = errno; free(lista); errno = e; return -1; }
    int esta = 0;
    for (int i = 0; i < obtenidos; ++i) if (lista[i] == gid) esta = 1;
    free(lista);
    return esta;
}

static void foto(const char *fase, const struct stat *s)
{
    printf("%s uid=%ju gid=%ju modo=%03o enlaces=%ju\n", fase,
           (uintmax_t)s->st_uid, (uintmax_t)s->st_gid,
           (unsigned)(s->st_mode & 0777), (uintmax_t)s->st_nlink);
}

int main(int argc, char **argv)
{
    gid_t grupo = getegid();
    if ((argc != 2 && argc != 3) ||
        (argc == 3 && leer_gid(argv[2], &grupo) == -1)) {
        fprintf(stderr, "Uso: %s DIRECTORIO_NUEVO [GID_DECIMAL_PROPIO]\n", argv[0]);
        return 2;
    }
    int miembro = pertenece(grupo);
    if (miembro == -1) { perror("consultar grupos"); return 1; }
    if (!miembro) {
        fputs("El GID solicitado no pertenece a los grupos actuales del proceso\n", stderr);
        return 2;
    }
    int dir = -1, fd = -1, estado = 1;
    struct stat inicial, actual, alias;
    mode_t anterior = umask(0077);
    if (mkdir(argv[1], 0700) == -1) goto error;
    dir = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir == -1) goto error;
    fd = openat(dir, "dato", O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
    if (fd == -1 || linkat(dir, "dato", dir, "alias", 0) == -1 ||
        fstat(fd, &inicial) == -1) goto error;
    foto("inicial", &inicial);
    if (fchown(fd, (uid_t)-1, grupo) == -1 || fstat(fd, &actual) == -1)
        goto error;
    if (actual.st_uid != inicial.st_uid || actual.st_gid != grupo) goto inesperado;
    foto("grupo", &actual);
    if (fchmod(fd, 0640) == -1 || fstat(fd, &actual) == -1 ||
        fstatat(dir, "alias", &alias, AT_SYMLINK_NOFOLLOW) == -1) goto error;
    if (actual.st_dev != alias.st_dev || actual.st_ino != alias.st_ino ||
        actual.st_uid != inicial.st_uid || actual.st_gid != grupo ||
        (actual.st_mode & 0777) != 0640 ||
        (alias.st_mode & 0777) != 0640 || alias.st_gid != grupo ||
        actual.st_nlink != 2 || alias.st_nlink != 2) goto inesperado;
    foto("final", &actual);
    foto("alias", &alias);
    puts("uid_conservado=correcto grupo=correcto modo=correcto identidad=correcta");
    estado = 0;
    goto fin;
inesperado:
    fputs("Propiedad, modo, enlaces o identidad inesperados\n", stderr);
    goto fin;
error:
    perror("operacion del objeto propio");
fin:
    umask(anterior);
    if (cerrar(&fd) == -1) { perror("close archivo"); estado = 1; }
    if (cerrar(&dir) == -1) { perror("close dir"); estado = 1; }
    if (fflush(stdout) == EOF) { perror("fflush stdout"); estado = 1; }
    return estado;
}
terminal · ejecutar con el GID efectivo actual
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ajustar-propio.c -o ajustar-propio
$ ./ajustar-propio ensayo-grupo

Si hay otro GID en id -G, puedes pasarlo como tercer argumento sobre un nombre de directorio distinto. El programa conserva todos sus objetos para inspección y no emplea cambios recursivos. Una denegación del sistema se informa sin reintentar con privilegios ni reinterpretarla como éxito.

11. Laboratorio y verificación

Usa Linux o WSL con GCC, Bash, GNU Coreutils y Python 3. Trabaja como cuenta ordinaria sobre los objetos nuevos del laboratorio. En WSL es preferible el directorio personal de Linux. Se presupone un sistema que admite cambios de modo y grupo y un padre sin ACL predeterminada.

Guarda los fuentes en el directorio privado creado en la sección 2 y compílalos. Las órdenes de las secciones 2 a 7 pueden ejecutarse en ese orden. Cada archivo o directorio de creación debe tener un nombre nuevo.

terminal · conservar el contexto del ensayo
$ id
$ umask
$ findmnt -T .
$ ./crear-con-mascara ensayo-077 077
$ ./ajustar-propio grupo-actual
$ stat -c '%n uid=%u gid=%g modo=%a enlaces=%h' -- grupo-actual/dato grupo-actual/alias

Si una fórmula no coincide y el programa informa otro modo, revisa ACL heredadas y el soporte del montaje. No se quita una ACL ni se cambia una política global automáticamente para hacer que el resultado parezca el del modelo. El tema 28 dará las herramientas para interpretar esa información.

Verificar creación, modificaciones explícitas y grupos propios

verificar-cambios.py · comprobaciones sin cambiar usuarios ni grupos del proceso
from pathlib import Path
import os
import stat
import subprocess
import tempfile

def ejecutar(*args, correcto=True):
    p = subprocess.run(args, capture_output=True, text=True)
    if correcto:
        assert p.returncode == 0, (args, p.stderr)
    return p

def modo(ruta):
    return stat.S_IMODE(Path(ruta).stat().st_mode)

base = Path(tempfile.mkdtemp(prefix="prueba-cambios-", dir="."))
for mascara in (0o000, 0o002, 0o022, 0o027, 0o033, 0o077, 0o123, 0o777):
    d = base / f"m-{mascara:03o}"
    p = ejecutar("./crear-con-mascara", str(d), f"{mascara:03o}")
    fotos = {}
    for linea in p.stdout.splitlines():
        if " pedido=" in linea:
            fase, resto = linea.split(" ", 1)
            fotos[fase] = dict(c.split("=", 1) for c in resto.split())
    for fase, pedido in [("archivo", 0o666), ("privado", 0o600), ("carpeta", 0o777)]:
        assert int(fotos[fase]["real"], 8) == pedido & ~mascara
    assert int(fotos["fchmod"]["real"], 8) == 0o640
    assert modo(d / "archivo") == 0o640
    assert modo(d / "privado") == 0o600 & ~mascara
    assert modo(d / "carpeta") == 0o777 & ~mascara
    assert modo(d) == 0o700
    q = ejecutar("./crear-con-mascara", str(d), "027", correcto=False)
    assert q.returncode != 0
    assert modo(d / "archivo") == 0o640

archivo = base / "simbolico.txt"
archivo.write_text("contenido\n")
os.chmod(archivo, 0o640)
for operacion, esperado in [("u+x,g+w", 0o760), ("g=u,o=", 0o770),
                            ("a-x", 0o660), ("u=rw,g=r,o=", 0o640),
                            ("a+X", 0o640)]:
    ejecutar("chmod", operacion, "--", str(archivo))
    assert modo(archivo) == esperado
os.chmod(archivo, 0o750)
ejecutar("chmod", "a+X", "--", str(archivo))
assert modo(archivo) == 0o751
d = base / "directorio-X"
d.mkdir()
os.chmod(d, 0o640)
ejecutar("chmod", "a+X", "--", str(d))
assert modo(d) == 0o751

# Bash limita el efecto de la máscara al subshell.
comando = 'umask 022; chmod 444 -- "$1"; chmod +w -- "$1"'
ejecutar("bash", "-c", comando, "prueba", str(archivo))
assert modo(archivo) == 0o644
comando = 'umask 022; chmod 444 -- "$1"; chmod a+w -- "$1"'
ejecutar("bash", "-c", comando, "prueba", str(archivo))
assert modo(archivo) == 0o666

grupos = sorted(set(os.getgroups()) | {os.getegid()})
elegidos = [os.getegid()]
otro = next((g for g in grupos if g != os.getegid()), None)
if otro is not None:
    elegidos.append(otro)
for i, grupo in enumerate(elegidos):
    d = base / f"grupo-{i}"
    p = ejecutar("./ajustar-propio", str(d), str(grupo))
    a, b = (d / "dato").stat(), (d / "alias").stat()
    assert a.st_gid == b.st_gid == grupo
    assert a.st_uid == b.st_uid == os.geteuid()
    assert (a.st_dev, a.st_ino) == (b.st_dev, b.st_ino)
    assert a.st_nlink == b.st_nlink == 2
    assert modo(d / "dato") == modo(d / "alias") == 0o640

ajeno = next(g for g in range(1, 100000) if g not in grupos)
d = base / "grupo-no-propio"
p = ejecutar("./ajustar-propio", str(d), str(ajeno), correcto=False)
assert p.returncode == 2 and not d.exists()
for mascara in ("888", "27", "-027", "1027"):
    d = base / "entrada-invalida"
    p = ejecutar("./crear-con-mascara", str(d), mascara, correcto=False)
    assert p.returncode != 0 and not d.exists()
for gid in ("-1", "1x", "4294967295", "999999999999999999999999999999"):
    d = base / "gid-invalido"
    p = ejecutar("./ajustar-propio", str(d), gid, correcto=False)
    assert p.returncode == 2 and not d.exists()

print("Correctos: máscaras, modos explícitos, X, clases, grupo e identidad de alias")
print("Árbol conservado:", base)
terminal · ejecutar las comprobaciones
$ python3 verificar-cambios.py

Los GID inválidos del verificador corresponden a los tipos habituales de Linux que usa el laboratorio; el programa también comprueba que el valor pueda representarse como gid_t y que no sea el centinela -1. No intenta asignar un grupo ajeno para probar una denegación del kernel: el propio contrato lo rechaza antes de crear el árbol.

Con máscara 0777, el subdirectorio carpeta queda con modo cero, pero su padre privado conserva 0700. El verificador consulta atributos sin entrar en él. Si quieres inspeccionar su interior vacío, ajusta únicamente esa carpeta propia mediante chmod, después de conservar el resultado original.

12. Errores y efectos que hay que comprobar

OperaciónErrores o condiciones habitualesComprobación útil
chmod/fchmodFalta de autorización, ruta inaccesible, solo lectura o atributos que impiden el cambio.Retorno y modo final; no deducir permiso de cambio solo por w del contenido.
chown/fchownUID/GID no autorizados, credenciales, soporte o políticas del sistema.UID, GID y modo después; algunos atributos pueden alterarse.
Crear bajo umaskEspacio, cuota, destino existente, permisos o ACL heredada.Objeto efectivamente creado y atributos observados.
Cambiar un árbol completoErrores después de modificar parte de los objetos.Registro de alcance y fallos; no hay reversión universal por un estado final distinto de cero.

EPERM y EACCES no son sinónimos perfectos. El primero puede expresar falta de autorización sobre la operación de metadatos o restricciones del objeto; el segundo puede venir de búsqueda denegada en la ruta. EROFS indica solo lectura y no se corrige porque los bits básicos parezcan permisivos.

Un cambio de grupo puede modificar qué clase selecciona otro proceso sin cambiar el texto de sus permisos. Conviene registrar UID, GID y modo juntos, especialmente si se comparan fotos tomadas antes y después de chown o chgrp.

Las consultas y modificaciones por ruta están sujetas a carreras si el árbol es mutable. Las referencias por descriptor de estos ensayos estabilizan el objeto abierto, pero se parte de un árbol privado y de confianza. El tema 29 desarrollará la coordinación entre comprobación y uso.

13. Simuladores de chmod y umask

Aplicar un modo explícito o varias cláusulas

El primer panel transforma los nueve bits básicos. Acepta un modo octal completo o cláusulas con clases explícitas u/g/o/a y operadores +, - o =. Admite rwx, X condicional y copia de una sola terna como g=u. No implementa toda la sintaxis de GNU chmod.

Transformación secuencial del modo

modo inicial y final de la transformación
CláusulaAntesAcción básicaDespuésTernas resultantes

Con 0640, u+x,g+w debe dar 0760. Cambia la expresión a g=u,o= para copiar la terna del propietario del modo inicial, sin conservar una relación futura. Prueba a+X con regular sin ejecución y con directorio. Para estudiar el orden, compara u+x,g+X con a-x,a+X.

Cada cálculo parte del campo inicial. No se aplican automáticamente los resultados anteriores. El panel admite hasta 16 cláusulas y rechaza expresiones fuera de su subconjunto sin publicar un resultado parcial. No incluye clases omitidas, bits especiales, ACL ni autorización para realizar el cambio.

Filtrar una petición y cambiar después el modo

El segundo panel calcula la creación sin ACL predeterminada. La columna de máscara indica qué permisos deben retirarse si se habían solicitado. Añade una comparación con chmod explícito para separar dos fases que suelen confundirse.

Petición & complemento de la máscara

creación frente a modificación explícita posterior
BitSolicitadoMarcado por umaskResultado al crearExplicación

Prueba máscara 033 con petición 666: debe resultar 644 y no 633. Con petición 600 y máscara 000 el resultado sigue siendo 600. Con máscara 777 se retiran todos los bits de una creación básica; solicitar después chmod 640 no vuelve a pasar por ese filtro.

Los paneles trabajan solo en memoria y no consultan ni modifican el equipo. La segunda fase supone que el cambio explícito está autorizado y se completa. No modela la excepción de ACL predeterminada ni afirma que cualquier aplicación pida 0666 para regulares y 0777 para directorios.

14. Elegir una política de creación y cambio

Una política clara especifica quién debe poder leer, escribir o atravesar, qué aplicación crea cada objeto y en qué directorio. Elegir una máscara sin saber qué modo solicita la aplicación deja incompleta la predicción.

ObjetivoDecisión típica que hay que definirComprobación posterior
Datos privados al nacerPetición restrictiva y contexto de creación controlado.Modo real, herencia y accesibilidad del prefijo.
Colaboración con un grupoGID pertinente, bits de grupo y política de herencia.GID de cada objeto y grupos vigentes de los procesos.
Directorios recorribles sin ejecutar documentosDistinguir tipos y usar reglas de búsqueda o X apropiadas.Modos de carpetas y regulares por separado.
Modo final concreto tras cambiar propiedadOrdenar las operaciones y comprobar efectos secundarios.UID, GID, modo y otros atributos pertinentes.

Crear inicialmente con permisos amplios y restringir después abre una fase en que otra aplicación podría acceder, si el entorno lo permite. Para datos privados, conviene que la propia creación y su directorio sean restrictivos; una corrección posterior no borra lo que pudo observarse antes.

La máscara no sustituye la autorización para cambiar metadatos ni impide que el propietario cambie después el modo de sus objetos. Tampoco transforma un árbol compartido en una política coherente si aplicaciones distintas publican objetos con solicitudes y atributos diferentes.

Qué debe quedar en el informe del ensayo

  1. Petición y contexto. Modo pedido, máscara, padre y posible ACL predeterminada.
  2. Propiedad y credenciales. UID/GID observados y grupos vigentes relevantes.
  3. Operación exacta. Creación, chmod numérico, cláusulas simbólicas, cambio de grupo o UID.
  4. Resultado real. Retorno y atributos consultados, no solo el valor esperado por una fórmula.
  5. Alcance del cambio. Objetos propios concretos, alias compartidos y errores que dejaron un estado parcial.

El siguiente tema añadirá setuid, setgid, sticky, ACL y atributos extendidos. Esas características explicarán resultados que no pueden reducirse al filtrado de nueve bits que acabamos de calcular.

15. Resumen

16. Preguntas de repaso y práctica

1. ¿Umask modifica los archivos que ya existen?

No de forma retrospectiva. Es una máscara del contexto de creación del proceso, no una orden para recorrer y cambiar objetos existentes.

2. ¿Chmod 640 suma permisos al modo anterior?

No en el caso básico de un regular sin bits especiales. Establece las ternas indicadas: rw- r-- ---.

3. ¿Qué diferencia hay entre g+w y g=w?

G+w añade escritura a la terna existente de grupo. G=w establece esa terna a escritura, retirando los otros permisos básicos de esa clase.

4. ¿Qué hace g=u?

Copia los permisos de la terna actual del propietario a la de grupo en esa fase. No crea una relación que sincronice futuros cambios.

5. ¿X mayúscula añade ejecución a todos los documentos?

No. La incluye si el objeto es directorio o ya tiene algún bit de ejecución, según la fase de aplicación de la cláusula.

6. ¿Por qué +w y a+w pueden diferir?

En GNU chmod, al omitir clases la umask protege ciertos bits frente a esa modificación. Con a se seleccionan todas explícitamente.

7. ¿La llamada chmod filtra el modo explícito con umask?

No como una creación. La influencia de la clase omitida es parte de la interpretación de la herramienta simbólica, no un filtro universal de la llamada al sistema.

8. ¿Cuánto resulta de pedir 0666 bajo máscara 0027?

0640 en el caso básico sin ACL predeterminada: se retiran escritura de grupo y todos los permisos de otros.

9. ¿Por qué 0666 menos 0033 no sirve para calcular el resultado?

Porque la máscara opera retirando bits. La resta produce 0633, mientras 0666 & ~0033 produce 0644.

10. ¿Una máscara 000 convierte una petición 0600 en 0666?

No. No retira bits, pero tampoco añade los que no se pidieron. El resultado sigue siendo 0600.

11. ¿Todo archivo regular se crea pidiendo 0666?

No. Es una petición común de ciertas herramientas. Una aplicación puede pedir otro modo, como 0600.

12. ¿Qué cambia si el padre tiene una ACL predeterminada?

En Linux la creación deriva permisos de esa herencia y los limita con el modo solicitado; no se aplica simplemente la fórmula de umask. Se desarrollará en el tema 28.

13. ¿Cambiar umask en un subshell cambia la del shell padre?

No. La prueba queda acotada a ese contexto y a sus descendientes. La sesión padre conserva su propia máscara.

14. ¿Umask(0) seguido de restauración es una consulta sin efectos intermedios?

No. Instala temporalmente otra máscara. En un contexto con creaciones concurrentes puede alterar sus resultados.

15. ¿Ser propietario permite asignar el archivo a cualquier UID?

No en Linux ordinario sin privilegios apropiados. Cambiar UID propietario y cambiar grupo tienen reglas distintas.

16. ¿Qué significa fchown(fd, (uid_t)-1, grupo)?

Solicita conservar el UID y cambiar el GID al indicado. El centinela -1 no se asigna como identidad propietaria.

17. ¿Cambiar grupo garantiza que el modo no haya variado?

No para todos los atributos y casos. Pueden limpiarse bits especiales o capacidades; se consulta el estado y se considera el orden de las operaciones.

18. ¿Un alias duro conserva permisos independientes?

No. Los nombres comparten el mismo objeto y sus atributos. El programa C comprueba modo y grupo a través del alias.

19. ¿W del contenido basta para autorizar chmod?

No. Modificar contenido y cambiar metadatos tienen reglas de autorización diferentes, asociadas a propiedad, capacidades y políticas.

20. ¿Un fallo recursivo significa que ningún modo cambió?

No. Pueden haberse modificado objetos antes del error. Hay que registrar alcance y resultados; no existe una reversión automática de todo el árbol.

Para practicar

  1. Parte de 0640 y calcula u+x,g+w, después g=u,o=, registrando cada fase.
  2. Compara a+X sobre un regular sin ejecución, otro que ya tenga x y un directorio.
  3. Comprueba la diferencia entre +w y a+w bajo umask 022 en un subshell.
  4. Calcula peticiones 0666, 0777 y 0600 bajo máscaras 000, 022, 027, 033 y 077.
  5. Construye otro contraejemplo donde restar la máscara produzca un modo incorrecto.
  6. Ejecuta crear-con-mascara y explica por qué fchmod logra 0640 aun con máscara 0777.
  7. Comprueba que cambiar la máscara no modifica un archivo que ya existía.
  8. Ejecuta ajustar-propio con el GID efectivo y, si existe, con otro GID suplementario propio.
  9. Observa el mismo modo y grupo por los dos nombres duros del ensayo.
  10. Prueba un GID no perteneciente al proceso y verifica que el programa lo rechaza antes de crear objetos.
  11. Describe qué información pedirías si un modo de creación no coincide con petición & ~umask.
  12. Diseña una política de creación privada que no dependa de restringir permisos amplios después de publicar datos.

En el tema 28 estudiaremos los bits setuid, setgid y sticky, las ACL, los atributos extendidos y las herramientas para consultar y modificar esas características.