2023/03/06

VirtualBox con Secure Boot

Hago esta nota más para no tener que buscar y recordar. Tomé todo de [1] y [2] y le pongo un poco de mi sabor, pues tengo mezcla con otro tema. De paso queda en spanish.


Tengo una máquina en modo dual boot con Windows 10 y bitlocker por un lado y Ubuntu 22 por el otro. En Windows 10, aparentemente por bitlocker, las virtuales con VirtualBox cada tanto le hacen tirar un BSOD. Mitiga un poco bajar en Execution Cap (de la virtual a usar, Settings -> System -> Processor -> Execution Cap) hasta el piso de lo verde, evitar hacer operaciones intensivas de disco y darle mucha memoria para que evite swappear, pero no alcanza, no puedo tener un BSOD en medio de una charla o taller.

Cuando instalé VirtualBox en Ubuntu, me encontré con un diálogo que nunca había visto, debido a que tiene activado Secure Boot.

No tomé nota bien de qué ofrecía o pedía, algo de una clave para UEFI o similar. Le dije que no, probablemente cancelé cerrando la terminal pues no me dejaba de otra manera. Luego, aun desinstalando y volviendo a instalar no volvió a ofrecerlo, así que no pude documentar.

 

Como efecto colateral me hizo reingresar la key de bitlocker al reiniciar, eso es, tras arruinarme el grub, de ahí en más, arranca directo con Windows.

Esto no es gran problema, cuando quiero arrancar con linux invoco el menú de boot del firmware que reconoce la partición UEFI Ubuntu, con eso se carga el grub y de ahí Ubuntu o si quisiera Windows.


Yendo a lo importante, al intentar ejecutar VirtualBox falla con un mensaje parecido a

 

modprobe vboxdrv failed. Please use 'dmesg' to find out why.
 

dmesg dice:

 

kernel: Lockdown: modprobe: unsigned module loading is restricted; see man kernel_lockdown.7

 

Buscando en internet llegué, tras varios artículos y respuestas diciendo "desactivá Secure Boot", que estupidéz, a uno que aunque bastante viejito es correcto.

Lo que dice es que si tenés Secure Boot, el kernel no te va a dejar insertar módulos que no estén firmados y brinda una manera de firmarlos. No voy a intentar explicar el detalle por que tengo una intuición pero no claridad.

Mi intuición es, para que un módulo esté firmado, hay que hacerlo con un secreto. Ese secreto hay que cargarlo en alguna zona protegida.

 

Esto genera el secreto:

 

$ openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -outform DER -out MOK.der -nodes -days 36500 -subj "/CN=VBox Cert/"

 

Esto firma el módulo con el secreto:

 

$ sudo /usr/src/linux-headers-$(uname -r)/scripts/sign-file sha256 ./MOK.priv ./MOK.der $(modinfo -n vboxdrv)

 

En mi caso la expansión es:

 

$ sudo /usr/src/linux-headers-5.15.0-48-generic/scripts/sign-file sha256 ./MOK.priv ./MOK.der /lib/modules/5.15.0-48-generic/updates/dkms/vboxdrv.ko

 

Lo mismo para hay que hacer para vboxnetflt.

Esto inicia el proceso de guardar el secreto en la zona protegida, es registrar la key con Secure Boot, te pide un secreto que establece la cadena de confianza rumbo al siguiente paso:

 

$ sudo mokutil --import MOK.der

 

Reiniciás:

 

$ shutdown -r now

 

Todo lo que sigue es la finalización del proceso de guardar el secreto.

Te aparecen unas pantallas ascii azul con un menú sencillo, en este elegís qué vas a hacer

 

Perform MOK management

+-----------------------+
|     Continue Boot     |
|      Enroll MOK       |
| Enroll key from disk  |
| Enroll hash from disk |
+-----------------------+

 

Te permite ver el secreto a registrar o continuar, continuar


[Enroll MOK]

+------------+
| View key 0 |
|  Continue  |
+------------+

 

¿Está usted seguro?

 

Enroll the key(s)?

+---+
|No |
|Yes|
+---+

 

Te pide el password que ingresaste previamente:


Enroll the key(s)?

+-------------------+
| Password:         |
+-------------------+

 

Listo

 

The system must now be rebooted

+--+
|OK|
+--+

 

Con esto se puede comprobar si está enrolado:

 

$ mokutil --test-key MOK.der
MOK.der is already enrolled
 

Con esto listás las keys:

 

$ mokutil --list-enrolled | grep -e "\[key" -e Issuer


[key 1]
        Issuer: C=GB, ST=Isle of Man, L=Douglas,
        O=Canonical Ltd.,
        CN=Canonical Ltd. Master Certificate Authority
[key 2]
        Issuer: CN=mint Secure Boot Module Signature key
[key 3]
        Issuer: CN=VBox Cert




Ese MOK.priv lo podés borrar del sistema tras resguardarlo, por ejemplo dentro del mismo keepass donde guardás las otras claves, en el campo de comentario. Al DER lo pasás antes a ascii con base64.




[1] https://askubuntu.com/questions/760671/could-not-load-vboxdrv-after-upgrade-to-ubuntu-16-04-and-i-want-to-keep-secur

[2] https://sourceware.org/systemtap/wiki/SecureBoot

2023/02/01

Procesamiento de CSV mayor a 2GB con node

 Problemas, problemas, siempre tengo problemas.


¿Cuál es mi problema esta vez?

Un archivo CSV de 2M5 líneas, que al tener saltos de línea válidos dentro de algunos campos llega a 18M líneas de texto, ocupando 3.1 GB.

Es un listado de algo que no puedo decir, pero sí que me conviene procesarlo con javascript para hacerle una transformación a algunos campos selectos y agregar otros desde unas tablas, tipo una desnormalización. Esto es para luego meterlo en otra herramienta y ahí extraerle unas estadísticas.

De paso, al archivo resultante, según el valor de un registro, partirlo en varios en formato CSV.


¿Por qué una librería especial en lugar de parsearlo así nomás? Ocurre que si tenemos un CSV simple, donde no hay valores en los campos que coincidan con los delimitadores ni saltos de línea, es muy sencillo, tipo:

console.log("hola,como,estas,vos".split(/,/));

[ 'hola', 'como', 'estas', 'vos' ]

Pero si el CSV fuera

"mensaje,'hola, como estás vos', '2023-01-08 14:14'"

quedaría:

[ 'mensaje', "'hola", " como estás vos'", " '2023-01-08 14:14'" ]

distinto al esperado, no le prestes mucha atención a las comillas, que es un problema adicional:

[ 'mensaje', "hola, como estás vos", "2023-01-08 14:14" ]


Y sumale los saltos de línea para los campos multilínea... para que esto funcione bien hay que usar caracteres de escape, un bolonqui, mejor usar alguna librería existente, como la muy recomendada csvtojson.

 

Se instala con:

npm install --save csvtojson

Luego, anticipando que va a haber problemas con el tamaño, mirando los ejemplos me decidí por procesar línea a línea.

 

const request=require('request')
const csv=require('csvtojson')
 
csv().fromStream(request.get('http://mywebsite.com/mycsvfile.csv'))
.subscribe((json)=>{
  return new Promise((resolve,reject)=>{
    // transform and output operation
  })
},onError,onComplete);

 

Como quiero levantar el archivo localmente, reemplacé request con un stream reader o como se llame.

const request=require('fs')
const csv=require('csvtojson')
 

csv().fromStream(fs.createReadStream(fileName))
.subscribe((json)=>{
  return new Promise((resolve,reject)=>{
    // transform and output operation
  })
},onError,onComplete);

Para lidiar con la asincronía, probablemente de modo incorrecto, hice esto, si alguien me señala cualquier error, me avisa por favor:

 

const csvtojsonV2=require('csvtojson/v2');
const fs=require('fs');

class Processor {
  async process(fileName) {
    let setTitle = true;
    await csvtojsonV2().fromStream(fs.createReadStream(fileName))
    .subscribe( (json)=> {
      return new Promise((resolve,reject)=> {
        if (setTitle) {
          console.log(
             Object.keys(json).map(e=>`'${e}'`).join(',')
          );
          setTitle = false;
        }
        // transform
        console.log(
            Object.values(json).map(e=>`'${e}'`).join(',')
        );
        resolve()
      })
    },this.onError,this.onComplete)
  }
}

module.exports = { Processor }; 

Y con 

const {Processor} = require('./Processor.js');
const processor = new Processor();

allRecordsFileName=process.argv[2];

async function run() {
  let result = await processor.processExport(allRecordsFileName);
}

run()

 

funciona bien hasta que no funciona:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Buscando rapidito, hallé que con:

export NODE_OPTIONS="--max-old-space-size=7168"

llega un poco más lejos y falla, pero con más swapping. Como que necesitaría más memoria según todas las entradas que encontré en Internet.

Para mi que necesitar más memoria es una completa estupidez, hace falta saber más, por que la idea misma de usar el stream es que vaya leyendo y descartando para que tenga un impacto reducido, no que lea todo el archivo en memoria.


Dicho sea de paso, leí todo el archivo en memoria y falló igual:

const csv=require('csvtojson')
csv().fromFile(csvFilePath)
.then((jsonObj)=>{
    jsonObj.forEach(line=> {
       console.log(line);
    }
    );
})

Voy apostando a una de estas, en este orden:

  • implementé mal lo del manejo de las Promises, muy probable pues no sé usarlas, vale tirarme alguna pista...
  • hice mal uso del stream reader, lo mismo...
  • csv2json tiene un error relacionado al stream reader

 

 Pero csv2json sabe manejar archvios grandes de algún modo, pues con:

./node_modules/csvtojson/bin/csvtojson \
     < input.3GB.csv \
     > output.json

aunque tardó horrores, lo hizo, sabe lidiar con más de 2GB, problema que ya veremos....

 

Plan B

 

Siempre recordemos que no podemos leer línea a línea un CSV y pretender que sea correcto, pero si el output.json anterior está listo, es de una sóla línea por registro. Ahora necesitamos leer:

require('output.json');

nop, falla:

RangeError [ERR_FS_FILE_TOO_LARGE]: File size (5734037250) is greater than 2 GB

La solución con stream, falla igual en el createStreamRead():

const fs       = require('fs');
const readline = require('readline');
const stream   = require('stream');

var inputFileName=process.argv[2];

var input  = fs.createReadStream(inputFileName);
var output = new stream();
var rl     = readline.createInterface(input,output);

rl.on('line',function(line) {
  var record = JSON.parse(line);
  console.log(line);
});

 

Plan C


Se puede leer con request tal como está el ejemplo original, por algo lo habrán puesto, ¿no?, pero, maldición, está deprecated!... no importa, funciona ok, pero no soporta file:///, así que hay que armar un servidor efímero:

sudo php -S 127.0.0.1:80

Cada vez menos elegante....