# o6h-memory-guard 1.0.2 Primera implementación para SmartOS global zone: daemon C de 64 bits, libkstat, /proc, identidad UUID + generación de zona + PID + instante de creación, configuración separada y servicio SMF. Entrega de código fuente; **no contiene un binario nativo validado en SmartOS**. Véase `VALIDATION.md`. Arranca en `dry-run`. El manifiesto añade `--force-dry-run`, por lo que cambiar sólo `ACTION_MODE` no arma el servicio. No se entrega ninguna zona autorizada. No se ha desplegado ni se han enviado señales reales durante la preparación. ## Documentación de la entrega - `README.md`: instalación, semántica, política, operación y rollback. - `VALIDATION.md`: validación local realizada al construir esta entrega y comprobaciones nativas que en ese momento quedaban pendientes. - `CHECKPOINT.md`: checkpoint operativo posterior, incorporado sin cambios, con la evidencia acumulada de compilación, ejecución y logging de v1.0.2 en SmartOS real, además del estado exacto y los requisitos antes de `armed`. - `CHANGELOG.md`: evolución de versiones y revisión documental del paquete. El checkpoint es cronológicamente posterior a `VALIDATION.md`; por ello amplía sus resultados de campo sin cambiar el código ni autorizar el modo `armed`. ## Instalación inicial y activación en dry-run Los comandos siguientes se ejecutan por el operador, como root en la global zone. Requieren `gcc` de 64 bits, cabeceras nativas y GNU make (`gmake`) en PATH. Si no hay herramientas de desarrollo, compilar en una **zona nativa SmartOS** de la misma plataforma (no LX), ejecutar allí `gmake test`, y copiar el binario junto a este árbol al host. No instalar un compilador en un nodo bajo presión. Copiar el archivo distribuido a `/var/tmp/o6h-memory-guard-1.0.2.tar.gz`: ```sh mkdir -p /opt/custom/src cd /opt/custom/src gzip -dc /var/tmp/o6h-memory-guard-1.0.2.tar.gz | tar -xf - cd /opt/custom/src/o6h-memory-guard gmake CC=gcc gmake test CC=gcc file ./o6h-memory-guard ldd ./o6h-memory-guard sh ./install.sh ``` `file` debe identificar un ELF nativo de 64 bits. `ldd` no debe mostrar dependencias ausentes. El instalador sólo copia archivos y valida el manifiesto; no importa ni inicia servicios, y rechaza sobrescribir una instalación existente. Si ya se copió un binario compilado en una zona nativa, omitir `gmake CC=gcc` en la global zone; conservar los resultados de compilación y pruebas. Inventariar UUID y alias fuera del daemon: ```sh vmadm list -o uuid,alias vi /opt/custom/etc/o6h-memory-guard.conf ``` Añadir una línea por zona, con UUID real y alias sin espacios: ```text zone web5 allow zone database protect zone monitoring protect ``` Esos marcadores **no son valores válidos**: sustituirlos, incluidos los signos `< >`. `allow` hace elegible una zona para las simulaciones; no habilita señales mientras el servicio esté en dry-run. `protect`, `observe` o la ausencia de una regla excluyen la zona de toda acción. Sin reglas `allow` el monitor funciona y registra métricas/top de procesos, pero informa `no_authorized_candidate`. ```sh /opt/custom/bin/o6h-memory-guard --check-config --force-dry-run /opt/custom/bin/o6h-memory-guard --probe --force-dry-run svccfg validate /opt/custom/smf/o6h-memory-guard.xml svccfg import /opt/custom/smf/o6h-memory-guard.xml svcadm enable -s svc:/site/o6h-memory-guard:default svcs -xv svc:/site/o6h-memory-guard:default svcprop -p start/exec svc:/site/o6h-memory-guard:default tail -f /opt/custom/var/log/o6h-memory-guard/guard.log ``` Verificar `mode=dry-run` en `event=start`, `--force-dry-run` en `start/exec`, `arc_ok=1`, `swap_committed_ok=1`, `swap_physical_used_ok=1`, `anon_pageout_ok=1` y `proc_scan complete=1`. No generar presión artificial en producción. Para comprobar selección y UUID, revisar `candidate`, `zone` y `top_process` durante carga natural; `WOULD_ACTION` sólo aparece si se alcanzan los umbrales y hay candidato autorizado. `--probe` lee una muestra y sale sin locks, logs ni acciones; `--check-config` sólo valida configuración y permisos. Para que se active automáticamente tras un reinicio de SmartOS, después de validar este arranque editar **únicamente** `create_default_instance` a `enabled="true"` en el manifiesto persistente y volver a importarlo: ```sh vi /opt/custom/smf/o6h-memory-guard.xml svccfg validate /opt/custom/smf/o6h-memory-guard.xml svccfg import /opt/custom/smf/o6h-memory-guard.xml ``` Mantener `--force-dry-run`. La ubicación `/opt/custom/smf` se utiliza para servicios personalizados persistentes de SmartOS; no depender exclusivamente del estado habilitado de la instancia en el repositorio del arranque actual. ## Métricas y ventanas Cada segundo se leen `unix:0:system_pages:{freemem,lotsfree,desfree,minfree,nscan, pp_kernel,pageslocked}`, `cpu:*:vm:anonpgout` y `zfs:0:arcstats:size` mediante libkstat. `swapctl(SC_AINFO)` proporciona asignación, reserva y disponibilidad de swap virtual, equivalente a `swap -s`. La métrica inequívoca es `swap_committed_pages = allocated + reserved`, e incluye compromiso anónimo que puede seguir residente en RAM; **no representa pageout ni bytes escritos en el zvol**. La reserva aún no asignada es `ani_resv - (ani_max - ani_free)`. Por separado, `swapctl(SC_LIST)` obtiene las mismas entradas que `swap -l`. `swap_physical_used_bytes` suma `(blocks - free) * 512` de todos los dispositivos; los campos nativos de la API están en páginas y se convierten primero a los bloques de 512 bytes que muestra la utilidad. Esta cifra sí representa ocupación física del swap configurado. Con la validación observada: `swap -s` indicó unos 28,5 GiB comprometidos, mientras `swap -l` indicó sólo 8.093.064 bloques usados, es decir 4.143.648.768 bytes (unos 3,86 GiB). No deben compararse como si fueran la misma magnitud. El log conserva unidades: páginas para VM/compromiso virtual/pageout, bytes para ARC y swap físico, KiB para RSS. `page_bytes` se obtiene de `sysconf`, nunca se presupone 4096. `nscan` es una medida del scanner, no un contador acumulativo; su delta se registra como cambio de la medida y no se interpreta como tasa de pageout. Historias circulares con timestamps monotónicos, deltas 5/15/60 s para libre, kernel, locked, ARC, compromiso/disponibilidad virtual, ocupación física de swap, pageout anónimo y nscan. Los deltas incluyen la edad real (`-100@5.02s`). Se exige una muestra a ±1,25 s de la ventana; si falta, sale `NA`, sin inventar ceros. /proc se recorre cada 5 s en NORMAL y cada 1 s bajo `lotsfree`; RSS y crecimiento de procesos/zonas usan las mismas ventanas. El contrato de cada `event=sample` expone los tres pares inequívocos `swap_committed_ok/swap_committed_pages`, `swap_physical_used_ok/swap_physical_used_bytes` y `anon_pageout_ok/anon_pageout_pages`. Cada muestra va seguida por líneas `event=delta` con esos tres nombres exactos y `d5`, `d15` y `d60`. `swap_used_pages` no es un nombre válido. Se conservan como diagnóstico auxiliar `swap_alloc_pages`, `swap_reserved_pages` y `swap_available_pages`: son los componentes de compromiso virtual de `SC_AINFO`, todos en páginas, y ninguno representa bytes escritos en el dispositivo swap. En los datos originales, `425950 > 130875` páginas y `nscan=0` corresponden a NORMAL. Los valores de 130875/65437/49077 páginas no están codificados como umbrales: se vuelven a leer del kernel. ARC y `pp_kernel` son diagnóstico, nunca una prueba de que un proceso concreto causó la presión. ## Estados y acción | Estado/condición de entrada | Comportamiento predeterminado | | --- | --- | | NORMAL | Sin intervención; muestreo VM de 1 s. | | PRE-PRESSURE | `free < lotsfree` continuo durante 3 s; atribución de 1 s. | | PRESSURE | Bajo `lotsfree` durante 10 s y scanner activo o libre descendiendo. | | CRITICAL | Bajo `desfree` con presión corroborada continua durante 2 s; TERM. | | EMERGENCY | `free < minfree`; entrada en la misma muestra, sin confirmación. TERM a nuevo candidato; KILL tras 1 s al pendiente. | | Floor | `free < minfree/2`; KILL directo o escalada inmediata al pendiente. | | RECOVERY | Al recuperar `lotsfree` se cancela la escalada. NORMAL requiere 15 s con `free >= lotsfree*1.10` y `nscan=0`. | La presión se corrobora con `nscan > 0`, caída respecto a la muestra anterior o delta libre de 5 s negativo. Además, bajo `lotsfree`, el crecimiento simultáneo en 5 s de `swap_physical_used_bytes` y `anon_pageout_pages` corrobora presión. La ocupación física jamás es un trigger único: sin pageout creciente y memoria bajo `lotsfree` no cambia el estado. El compromiso virtual tampoco se interpreta como pageout. Los tiempos son segundos monotónicos transcurridos, no número de filas leídas. Un hueco de más de 2,5 s reinicia las confirmaciones. La gravedad se retiene hasta recuperación; `action_pressure` indica si **la condición actual** permite actuar. Por eso un estado retenido CRITICAL no autoriza una señal por sí solo. TERM tiene 5 s de gracia en CRITICAL. Sólo se escala si la condición crítica sigue confirmada y la lectura fresca sigue bajo `desfree`. Por encima de `desfree` se suspenden señales; alcanzar `lotsfree` cancela el pendiente. Tras KILL se puede elegir otro candidato si sigue la presión. EMERGENCY evita la espera normal entre víctimas de 10 s. Se mantiene como máximo una decisión de señal por tick; un pendiente desaparecido puede sustituirse en ese mismo tick. Hay un máximo predeterminado de 4 víctimas por episodio y 4 por ventana móvil de 60 s, tanto en simulación como en armed. TERM+KILL del mismo proceso cuentan como una víctima. El presupuesto del episodio sólo se reinicia al volver a NORMAL; el de 60 s sobrevive a ese cambio. En armed, un arranque o reinicio tiene 60 s de inhibición obligatoria de señales (también en EMERGENCY), para evitar eludir el límite por minuto mediante reinicios repetidos. Los contadores son en memoria; el presupuesto por episodio sí se reinicia al reiniciar el daemon. Revisar esta elección antes de usarlo como mecanismo de rescate. No hay garantía de evitar un panic. ## Atribución y protecciones Se suma RSS por UUID/generación de zona y se puntúa como `RSS + 2*max(crecimiento,0)`, usando preferentemente 60 s, después 15 s y 5 s. Se elige la zona elegible con mayor puntuación y dentro de ella el proceso elegible con mayor puntuación. Sin historia se usa RSS. Empates conservan el primer candidato encontrado. **La suma de RSS puede contar memoria compartida varias veces**: es una aproximación de atribución, no memoria física exclusiva ni memoria garantizada que se recuperará al terminar un proceso. El UUID es el nombre de zona devuelto por el kernel, validado como UUID canónico en minúscula (convención SmartOS). Se conserva además `ZONE_ATTR_UNIQID`; se consulta `ZONE_ATTR_INITPID`. Los alias son etiquetas del catálogo del operador, no se obtienen mediante comandos ni se usan para autorizar señales. Un alias renombrado debe actualizarse manualmente. En illumos con nombres de zona que no sean UUID, el muestreo funciona pero esas zonas quedan excluidas de acciones. Protecciones obligatorias: global zone completa; PID 0/1 y proceso propio; padre 0; procesos sin LWPs; init de cada zona; identidades no resueltas o sin generación; zonas sin regla `allow`; RSS menor de 64 MiB por defecto. También se excluyen los nombres `init`, `systemd`, `svc.startd`, `svc.configd`, `sshd`, `zoneadmd`, `vminfod`, `vmadmd`, `qemu`, `qemu-system-x86`, `bhyve` y el guard. Las protecciones se aplican incluso al floor y prevalecen sobre toda autorización. `protect_process` añade nombres exactos `pr_fname` de hasta 15 caracteres, sin comodines ni argumentos; `protect_pid` añade PID global, no PID visto dentro de LX. La config entregada añade PostgreSQL, MySQL/MariaDB, Redis y Zabbix. Las zonas de base de datos/infraestructura deben figurar como `protect`. Los nombres pueden cambiar/truncarse y no constituyen una frontera contra un proceso malicioso: la protección por UUID completo es la más robusta. Antes de actuar se comprueban UUID, generación, PID, inicio, nombre, UID/EUID, init, RSS y reglas. Se abren psinfo/ctl respecto al mismo descriptor de directorio /proc y se envía `PCKILL` al descriptor, no a un número PID mediante `kill(2)`. El inicio de un PID reutilizado invalida el candidato. Cambios de nombre/UID ya visibles también lo invalidan. Queda la carrera inherente de un exec ordinario entre la última lectura de psinfo y la escritura: no se detiene el proceso para eliminarla. No se tocan flags de tracing, no se usa kill-on-close ni señales a grupos. La integración de esta ruta todavía necesita validación nativa de laboratorio. En dry-run nunca se abre `ctl`: se revalida con lecturas, se escribe `WOULD_ACTION=SIGTERM/SIGKILL` y se actualiza sólo el estado simulado. Los presupuestos evitan repetir infinitamente el mismo sacrificio hipotético. En armed se escribe `ACTION_INTENT` antes y `action_result=...` después (el campo real es `event=action_result result=sent|failed`). Los errores de escritura de señal consumen presupuesto para evitar reintentos ambiguos. ## Recursos y fallos Almacenamiento de trabajo fijo para 4096 procesos y 256 zonas, prefaulted por inicialización; el tamaño se registra al arrancar. No hay malloc de la aplicación en la ruta de acción, ni `system`, `popen`, `exec`, `vmadm`, `ps` o `vmstat` en el daemon. libkstat/libc sí pueden reservar memoria internamente al actualizar su cadena. No se promete cero asignaciones ni tiempo real estricto. El recorrido /proc tiene presupuesto cooperativo de 200 ms (hasta 500 ms por configuración). Si faltan permisos, se excede capacidad/plazo o hay errores, el scan se marca incompleto y bloquea acciones. La desaparición normal de un proceso no se trata como fallo total. Se usa caché completa de como máximo 2 s en emergencias y siempre se revalida. Si no hay caché válida se intenta el recorrido antes de actuar. Una llamada del kernel bloqueada puede exceder el presupuesto; no es un límite duro. Los overruns quedan registrados. Muestras obligatorias ausentes/invalidas o demasiado lentas inhiben señales; ARC/swap/pageout opcionales ausentes se marcan con `*_ok=0` y no bloquean por sí solos. Un fallo de log inhibe las acciones hasta reiniciar y también se anuncia en stderr/SMF. Los logs se escriben síncronamente al archivo sin fsync por muestra; el I/O o ZFS bloqueado puede retrasar este daemon. Tampoco garantiza que las últimas líneas sobrevivan a pérdida de alimentación o panic. Es persistencia en disco, no un journal de transacciones de señales. No se cambia swap, ARC ni caps de zona. La enumeración física admite hasta 64 dispositivos swap con almacenamiento fijo; si el nodo supera ese límite, `swap_physical_used_ok=0` y esa señal diagnóstica se omite sin autorizar acciones. Un lock fcntl evita dos instancias (incluso una manual y otra SMF); el PID guardado es diagnóstico, nunca se usa para señalizar. Config/locks/logs exigen propietario root, archivo regular de un enlace, destino sin symlink y directorios resueltos sin escritura para grupo/otros. Directorios /opt con symlinks legítimos de root se admiten al resolver su destino. SIGHUP sólo reabre el log; la configuración se recarga reiniciando el servicio. SIGTERM/SIGINT al daemon solicitan salida limpia. ## Logging y mantenimiento ```text /opt/custom/bin/o6h-memory-guard /opt/custom/etc/o6h-memory-guard.conf /opt/custom/smf/o6h-memory-guard.xml /opt/custom/var/run/o6h-memory-guard.lock /opt/custom/var/log/o6h-memory-guard/guard.log /opt/custom/var/log/o6h-memory-guard/guard.log.1 ... guard.log.5 ``` Rotación interna: 10 MiB por archivo y 5 copias, aproximadamente 60 MiB máximos. El volumen de líneas por segundo puede hacer que la retención sea inferior a un día; aumentar LOG_MAX_MIB/LOG_KEEP o recoger logs externamente si se necesita un estudio de varios días. No configurar copytruncate/logadm simultáneamente. Snapshots de todas las zonas y top 20 procesos cada 60 s normal / 5 s en presión. Las muestras de 1 s posteriores a las acciones permiten revisar la recuperación en 1/2/5/10 s, siempre que no haya overruns. No se registran argumentos de procesos. `logs/policy-tests.log` y `logs/runtime-tests.log` proceden de pruebas locales; no son datos del nodo. `logs/example.log` es un ejemplo sintético de formato. ## Preparación de armed (no ejecutar durante la evaluación inicial) Se necesitan deliberadamente dos cambios: `ACTION_MODE armed` en la config y retirar `--force-dry-run` del método start del manifiesto. Mantener las reglas UUID `allow` mínimas y verificar antes las protecciones y el laboratorio nativo. Después de esos cambios, validar config/manifiesto, importar, refrescar y reiniciar el servicio. Verificar `event=start mode=armed`; comienza la inhibición de 60 s. No se proporciona un script que arme automáticamente el servicio. Para volver a dry-run, restaurar `ACTION_MODE dry-run` y `--force-dry-run` en el manifiesto; importar, `svcadm refresh` y `svcadm restart` la instancia. ## Parada, rollback y actualización Parar el guard sin borrar evidencia: ```sh svcadm disable -s svc:/site/o6h-memory-guard:default svcs -p svc:/site/o6h-memory-guard:default ``` Si se habilitó el arranque persistente, volver a `enabled="false"` e importar el manifiesto para mantenerlo deshabilitado en próximos arranques. Para retirar el servicio conservando los archivos y logs: ```sh mkdir -p /opt/custom/var/backups/o6h-memory-guard mv /opt/custom/smf/o6h-memory-guard.xml /opt/custom/var/backups/o6h-memory-guard/o6h-memory-guard.xml.disabled svccfg delete svc:/site/o6h-memory-guard:default ``` Comprobar antes que el destino de backup no exista y elegir un nombre distinto si ya se utilizó. El manifiesto sale del directorio de importación automática; binario/config/logs permanecen recuperables. No eliminar ni truncar el lock de una instancia viva. Restaurar el manifiesto e importarlo permite reinstalar SMF. Para actualizar: deshabilitar primero, conservar copias del binario, config y manifiesto en un directorio de backup nuevo, reemplazar el binario estando parado y validar los tres archivos. No reemplazar automáticamente la config ni perder las protecciones. El instalador inicial se niega a hacerlo por ese motivo. ## Referencias revisadas - [SmartOS: servicios personalizados y /opt/custom/smf](https://docs.smartos.org/smf-quick-reference-gz/). - [illumos: libkstat](https://smartos.org/man/3KSTAT/kstat). - [illumos: definiciones nativas de psinfo](https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/procfs.h). - [OmniOS/illumos: control /proc y PCKILL](https://man.omnios.org/man5/proc.5). - [illumos: atributos de zona](https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/zone.h). - [OmniOS/illumos: resolución del nombre de zona](https://man.omnios.org/man3c/getzoneid). - [illumos: cálculo de swap -s](https://github.com/illumos/illumos-gate/blob/master/usr/src/cmd/swap/swap.c). - [illumos: listado swap -l y conversión a bloques](https://github.com/illumos/illumos-gate/blob/master/usr/src/cmd/swap/swap.c#L350-L474). Las interfaces de atributos de zona dependen de la plataforma: confirmar con la versión exacta de SmartOS. La documentación revisada no sustituye la prueba de compilación/ejecución en ese host.