Archivo de la categoría: tutoriales

A ritmo de conga (2)

Actualizado: varios comandos de servidor a aspiradora terminaban con un LF. Añadidos.

DISCLAIMER: no seré responsable si alguien decide seguir mis pasos y se carga su aspiradora. En principio todo lo que cuento debería ser seguro, pero por motivos obvios no me puedo responsabilizar de lo que hagan otras personas, sólo de lo que haga yo.

Ahora que ya sabía la estructura básica del protocolo de la aspiradora, decidí escribir un pequeño programa en Python que me extrajese la información de manera más clara, y así no tener que andar con Wireshark, que es bastante peñazo. Con él tengo las secuencias y el orden en que se transmiten disponibles de un vistazo. El código se puede bajar desde mi web.

Lo primero que hice fue apagar completamente la aspiradora para que se borrase todo lo que tuviese en memoria, y emparejarla de nuevo, a ver si cambiaba algo en el proceso. Tras comparar ambas, todo es igual excepto:

  • el campo nonce
  • el campo sign (aunque esto es, probablemente, porque actualicé el firmware)
  • el campo token
  • el campo authcode

A mayores, la aspiradora envió una petición HTTP extra a POST /baole-web/common/uploadLog.do HTTP/1.1 con información de un fallo, pero no parece nada importante.

Siguiendo con el análisis, decidí que tocaba empezar a enviar comandos, así que inicié la captura de paquetes y ordené a la aspiradora que comenzase a funcionar. Este fue el resultado (el campo targetId es, en realidad, un valor hexadecimal aparentemente aleatorio, pero único para cada aspiradora, así que lo anonimizo aquí. Es el mismo valor todas las veces que aparece). El número decimal es el instante de tiempo (en segundos) desde que empecé a registrar los datos, y el sentido del paquete se indica con s->a si es de servidor a aspiradora, a->s si es de aspiradora a servidor:

3.5093190670013428
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1a 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"yyyyyy"
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3"
  },
  "seq":0,
  "value":{"transitCmd":"100"}
}\n

3.6597180366516113
a->s db 01 00 00 | fa 00 00 00 | 01 00 00 00 | 1a 27 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3",
    "broadcast":"0"
  },"value":{
    "noteCmd":"102",
    "workState":"5",
    "workMode":"0",
    "fan":"2",
    "direction":"0",
    "brush":"2",
    "battery":"100",
    "voice":"2",
    "error":"0",
    "standbyMode":"1",
    "waterTank":"40",
    "clearComponent":"0",
    "waterMark":"0",
    "version":"3.11.416(513)",
    "attract":"0",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "cleanGoon":"2",
    "extParam":"{\"cleanModule\":\"3\"}"
  }
}

3.7990760803222656
a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 1a 00 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"0",
    "targetType":"6",
    "broadcast":"0"
  },"value":{
    "noteCmd":"102",
    "workState":"5",
    "workMode":"0",
    "fan":"2",
    "direction":"0",
    "brush":"2",
    "battery":"100",
    "voice":"2",
    "error":"0",
    "standbyMode":"1",
    "waterTank":"40",
    "clearComponent":"0",
    "waterMark":"0",
    "version":"3.11.416(513)",
    "attract":"0",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "cleanGoon":"2",
    "extParam":"{\"cleanModule\":\"3\"}"
  }
}

3.883021116256714
s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 1a 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

4.672595024108887
a->s 14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 1b 00 00 00 | e7 03 00 00

4.731273174285889
s->a 14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 1b 00 00 00 | e7 03 00 00

6.818763017654419
a->s bd 01 00 00 | 18 00 00 00 | 01 00 00 00 | 1c 00 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"0",
    "targetType":"6",
    "broadcast":"0"
  },"value":{
    "noteCmd":"102",
    "workState":"1",
    "workMode":"11",
    "fan":"2",
    "direction":"0",
    "brush":"2",
    "battery":"100",
    "voice":"2",
    "error":"0",
    "standbyMode":"1",
    "waterTank":"40",
    "clearComponent":"0",
    "waterMark":"0",
    "version":"3.11.416(513)",
    "attract":"0",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "cleanGoon":"2",
    "extParam":"{\"cleanModule\":\"3\"}"
  }
}

6.9016029834747314
s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 1c 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

8.281097173690796
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1c 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"zzzzzz",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3"
  },
  "seq":0,
  "value":{"transitCmd":"131"}
}\n

8.425811052322388
a->s 54 01 00 00 | fa 00 00 00 | 01 00 00 00 | 1c 27 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3",
    "broadcast":"0"
  },"value":{
    "transitCmd":"132",
    "doTime":"614",
    "clearArea":"0",
    "clearTime":"0",
    "clearModule":"11",
    "clearSign":"2020-06-24-01-31-41-2",
    "chargerPos":"-1,-1",
    "map":"AAAAAAAAZABk0vwAKtgACtgAKtPVAA==",
    "track":"AQABADIx"
  }
}

12.955074071884155
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1f 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"zzzzzz",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3"
  },
  "seq":0,
  "value":{"transitCmd":"131"}
}\n

13.06220006942749
a->s 5c 01 00 00 | fa 00 00 00 | 01 00 00 00 | 1f 27 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3",
    "broadcast":"0"
  },"value":{
    "transitCmd":"132",
    "doTime":"619",
    "clearArea":"0",
    "clearTime":"5",
    "clearModule":"11",
    "clearSign":"2020-06-24-01-31-41-2",
    "chargerPos":"-1,-1",
    "map":"AAAAAAAAZABk0vwAKoDXAApA1wAqgNPUAA==",
    "track":"AQACADIxMzE="
  }
}

Bueeeeeeno… menuda parrafada. Sin embargo empezamos a sacar ya cosas en claro: para poner en marcha la aspiradora y que comience a limpiar, la clave parece estar en este bloque, que parece servir para enviar un comando específico: en este caso, el servidor envía un paquete con el comando 100, como se especifica en transitCmd.

s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1a 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"yyyyyy",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3"
  },
  "seq":0,
  "value":{"transitCmd":"100"}
}\n

Otro detalle interesante es que siempre que la aspiradora envía un paquete, su séptimo byte es 00, pero si lo envía el servidor es C8. Esto también se cumplía en las comunicaciones anteriores, salvo en los paquetes ping, los que aparentemente sólo son necesarios para mantener la comunicación. A pesar de todo, esa parte aún va a requerir más investigación.

Una vez que la aspiradora está en marcha, el servidor envía repetidamente el comando 131, el cual parece que pide a la aspiradora que devuelva información sobre su posición y estado actual.

La darle a la opción de dejar de aspirar, los comandos que se intercambian son:

2.3617970943450928
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 23 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"yyyyyy",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3"
  },
  "seq":0,
  "value":{"transitCmd":"102"}
}\n

2.4898791313171387
a->s dc 01 00 00 | fa 00 00 00 | 01 00 00 00 | 23 27 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3",
    "broadcast": "0"
  },
  "value": {
    "noteCmd": "102",
    "workState": "1",
    "workMode": "11",
    "fan": "2",
    "direction": "0",
    "brush": "2",
    "battery": "100",
    "voice": "2",
    "error": "0",
    "standbyMode": "1",
    "waterTank": "40",
    "clearComponent": "0",
    "waterMark": "0",
    "version": "3.11.416(513)",
    "attract": "0",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "cleanGoon": "2",
    "extParam": "{\"cleanModule\":\"3\"}"
  }
}

2.6665220260620117
a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 1d 00 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "0",
    "targetType": "6",
    "broadcast": "0"
  },
  "value": {
    "noteCmd": "102",
    "workState": "2",
    "workMode": "0",
    "fan": "2",
    "direction": "0",
    "brush": "2",
    "battery": "100",
    "voice": "2",
    "error": "0",
    "standbyMode": "1",
    "waterTank": "40",
    "clearComponent": "0",
    "waterMark": "0",
    "version": "3.11.416(513)",
    "attract": "0",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "cleanGoon": "2",
    "extParam": "{\"cleanModule\":\"3\"}"
  }
}

2.751024007797241
s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 1d 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

4.319652080535889
a->s 84 01 00 00 | 14 00 00 00 | 01 00 00 00 | 1e 00 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "0",
    "targetType": "6",
    "broadcast": "0"
  },
  "value": {
     "noteCmd": "101",
    "clearArea": "0",
    "clearTime": "10",
    "clearSign": "2020-06-24-01-31-41-2",
    "clearModule": "11",
    "isFinish": "1",
    "chargerPos": "-1,-1",
    "extParam": "{\"fan\":\"2\",\"waterTank\":\"40\",\"mode\":\"0\"}",
    "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==",
    "track": "AQAEADIxMzExMTEy"
  }
}

El comando para detenerse parece ser el 102, lo que es consistente con que el de iniciar sea el 100.

Por último, si pulsamos la opción de volver a la estación base, ésto es lo que sale:

0.530087947845459
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 d6 4c | 25 27 00 00 | 00 00 00 00
{
  "cmd": 0,
  "control": {
    "authCode": "yyyyyy",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3"
  },
  "seq": 0,
  "value": {
    "transitCmd": "104"
  }
}\n

0.6716470718383789
a->s db 01 00 00 | fa 00 00 00 | 01 00 00 00 | 25 27 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3",
    "broadcast": "0"
  },
  "value": {
    "noteCmd": "102",
    "workState": "2",
    "workMode": "0",
    "fan": "2",
    "direction": "0",
    "brush": "2",
    "battery": "100",
    "voice": "2",
    "error": "0",
    "standbyMode": "1",
    "waterTank": "40",
    "clearComponent": "0",
    "waterMark": "0",
    "version": "3.11.416(513)",
    "attract": "0",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "cleanGoon": "2",
    "extParam": "{\"cleanModule\":\"3\"}"
  }
}

0.8249011039733887
a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 20 00 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "0",
    "targetType": "6",
    "broadcast": "0"
  },
  "value": {
    "noteCmd": "102",
    "workState": "4",
    "workMode": "0",
    "fan": "1",
    "direction": "0",
    "brush": "2",
    "battery": "100",
    "voice": "2",
    "error": "0",
    "standbyMode": "1",
    "waterTank": "40",
    "clearComponent": "0",
    "waterMark": "0",
    "version": "3.11.416(513)",
    "attract": "0",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "cleanGoon": "2",
    "extParam": "{\"cleanModule\":\"3\"}"
  }
}

0.8965139389038086
s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 20 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

2.895132064819336
a->s 83 01 00 00 | 14 00 00 00 | 01 00 00 00 | 21 00 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "0",
    "targetType": "6",
    "broadcast": "0"
  },
  "value": {
    "noteCmd": "101",
    "clearArea": "0",
    "clearTime": "15",
    "clearSign": "2020-06-24-01-31-41-2",
    "clearModule": "0",
    "isFinish": "0",
    "chargerPos": "49,49",
    "extParam": "{\"fan\":\"1\",\"waterTank\":\"40\",\"mode\":\"0\"}",
    "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==",
    "track": "AQAEADIxMzExMTEy"
  }
}

2.9380459785461426
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 27 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"yyyyyy",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "transitCmd":"131"
  }
}\n

3.048809051513672
a->s 64 01 00 00 | fa 00 00 00 | 01 00 00 00 | 27 27 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3",
    "broadcast": "0"
  },
  "value": {
    "transitCmd": "132",
    "doTime": "643",
    "clearArea": "0",
    "clearTime": "15",
    "clearModule": "0",
    "clearSign": "2020-06-24-01-31-41-2",
    "chargerPos": "49,49",
    "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==",
    "track": "AQAEADIxMzExMTEy"
  }
}

7.939983129501343
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 2a 27 00 00 | 00 00 00 00
{
  "cmd": 0,
  "control": {
    "authCode": "yyyyyy",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3"
  },
  "seq": 0,
  "value": {
    "transitCmd": "131"
  }
}\n

8.04991602897644
a->s 6c 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2a 27 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3",
    "broadcast": "0"
  },
  "value": {
    "transitCmd": "132",
    "doTime": "648",
    "clearArea": "0",
    "clearTime": "20",
    "clearModule": "0",
    "clearSign": "2020-06-24-01-31-41-2",
    "chargerPos": "50,50",
    "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==",
    "track": "AQAGADIxMzExMTEyMjIyMQ=="
  }
}

12.952444076538086
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 2c 27 00 00 | 00 00 00 00
{
  "cmd": 0,
  "control": {
    "authCode": "yyyy",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3"
  },
  "seq": 0,
  "value": {
    "transitCmd": "131"
  }
}\n

13.123134136199951
a->s 70 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2c 27 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType": "3",
    "broadcast": "0"
  },
  "value": {
    "transitCmd": "132",
    "doTime": "653",
    "clearArea": "0",
    "clearTime": "25",
    "clearModule": "0",
    "clearSign": "2020-06-24-01-31-41-2",
    "chargerPos": "50,48",
    "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNgABdLjAA==",
    "track": "AQAHADIxMzExMTEyMjIyMTIz"
  }
}

13.785572052001953
a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 22 00 00 00 | 00 00 00 00
{
  "version": "1.0",
  "control": {
    "targetId": "0",
    "targetType": "6",
    "broadcast": "0"
  },
  "value": {
    "noteCmd": "102",
    "workState": "5",
    "workMode": "0",
    "fan": "2",
    "direction": "0",
    "brush": "2",
    "battery": "100",
    "voice": "2",
    "error": "0",
    "standbyMode": "1",
    "waterTank": "40",
    "clearComponent": "0",
    "waterMark": "0",
    "version": "3.11.416(513)",
    "attract": "0",
    "deviceIp": "192.168.18.3",
    "devicePort": "8888",
    "cleanGoon": "2",
    "extParam": "{\"cleanModule\":\"3\"}"
  }
}

13.849518060684204
s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 22 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

Y, efectivamente, el comando es el 104. ¿Serán pares todos los comandos?

Además, parece que cuando la aspiradora ha llegado al cargador, emite automáticamente una trama para indicarlo (la del instante de tiempo 13.785572052001953). También vemos que el servidor envía repetidamente el comando 131 durante el trayecto, lo que parece confirmar que, efectivamente, se utiliza para pedir la posición y estado de la aspiradora mientras se está moviendo.

Y con esto ya están analizados los tres comandos más importantes. En la próxima entrega intentaré enviar yo mis propios comandos haciéndome pasar por el servidor, y en la cuarta espero analizaré el resto de comandos: selección del modo de aspirado, potencia del motor, fregona y control manual.

Parte 3

A ritmo de conga (1)

Actualizado: varios comandos de servidor a aspiradora terminaban con un LF (o sea, \n). Añadidos.

Recientemente decidí comprarme un aspirador robótico, así que empecé a ver opciones. Es cierto que, siendo el primero, no quería gastarme mucho por si no me convencía, pero a la vez no quería tampoco comprar algo cutre que luego no funcionase. Así que al final, después de mucho pensar, decidí comprarme un Conga de Cecotec. Mi intención era comprar el más bajo de gama, pero estaba agotado, así que al final me pillé el Conga 1490, que tiene como característica diferenciadora que se puede controlar desde el móvil.

Y claro, me ponen un caramelito así, delante, y lo primero que se me pasó por la cabeza fue «no estaría mal meter las narices a ver cómo funciona».

Sin embargo, cuando me leí las condiciones de uso de la app y descubrí que envía tu IP, tu correo-e y los mapas que hace la aspiradora no sólo a Cecotec (que, a fin de cuentas, es una empresa española y está sujeta a las normas comunitarias de protección de datos), sino directamente al verdadero fabricante, que es chino (y te dicen claramente que él sí que no está sujeto a esas normas), la cosa empezó a mosquearme, y mis ganas de tocar dentro aumentaron bastante.

Pero no se vayan todavía, que aún hay más… resulta que se me ocurre ver qué permisos pide para funcionar, y me encuentro con que en el manifiesto pide, entre otras cosas:

  • hacer llamadas
  • acceso total a Internet
  • acceso al micrófono
  • acceso a la cámara y al flash
  • acceso de lectura y escritura a todo el almacenamiento del dispositivo
  • obtener datos del propietario
  • acceso a los logs del sistema
  • acceso a la lista de aplicaciones
  • arrancar automáticamente en el inicio del sistema

Esto ya me parecía el colmo: una aplicación china con acceso a TODO, y que encima se lanza automáticamente en el arranque… ni-de-co-ña.

Y sí, se lo que dirán algunos: la mitad de esos permisos (cámara, micrófono, llamadas y almacenamiento) no sólo se pueden desactivar en las propiedades de la aplicación, sino que, de intentar usarlos, el sistema operativo avisaría; y la otra mitad (acceso a datos del propietario y lista de aplicaciones) han sido eliminados en versiones recientes de Android. Pero pese a todo, es una cuestión de que no basta con ser bueno, también hay que parecerlo. Y esta app, por mucho de que en la práctica no haga nada malo, que dejen todo eso así queda, como mínimo, feo.

Pero es que, además, como veremos luego, la app y la aspiradora precisan de una conexión a internet para trabajar, lo que significa que si, por el motivo que sea, el fabricante decide que ya no le compensa tener encendido su servidor, MI aspiradora, por la que he pagado dinero, dejará de funcionar de la manera que me vendieron que funcionaría.

Así que toca arremangarse y empezar a trabajar.

DISCLAIMER: no seré responsable si alguien decide seguir mis pasos y se carga su aspiradora. En principio todo lo que cuento debería ser seguro, pero por motivos obvios no me puedo responsabilizar de lo que hagan otras personas, sólo de lo que haga yo.

Lo primero que hice fue echar un vistazo al manual, y como cabía suponer, la aspiradora tiene WiFi y hay que emparejarla primero con el router. Para ello hay dos maneras, la automática y la manual. En ambos casos se comienza pulsando el botón de encendido de la aspiradora durante tres segundos, de manera que el piloto de la WiFi empieza a parpadear, y a continuación se mete la clave de la WiFi en la app (el SSID ya aparece, poniendo por defecto el mismo que usa el móvil).

Ahora, en modo automático, simplemente se deja que sea la propia app quien configure todo. Sin embargo, en modo manual hay que ir a Ajustes -> WiFi en el móvil y escoger una red con el identificador CongaGyro_XXXXXX, pulsar el botón de configurar, y volver a la WiFi normal. Efectivamente: al ponerse en modo emparejamiento, la aspiradora se convierte en un punto de acceso sin clave.

El primer paso, por tanto, era obvio: poner la aspiradora en modo emparejamiento, conectarse a esa WiFi desde el portátil (en mi caso me dio la IP 192.168.4.2), y hacer un escaneo de puertos con nmap:

nmap -p 1-49151 192.168.4.1

Y tal y como cabía esperar, el puerto 80 está abierto, y al entrar desde un navegador aparece esta página:

Tras escribir el SSID y la clave de mi WiFi, devuelve una página con un OK y el punto de acceso desaparece. Una simple comprobación con nmap nos devuelve la nueva IP de la aspiradora, ahora sí en la WiFi de verdad.

nmap -sP 192.168.18.0/24

Por supuesto, cada uno tiene que cambiar la IP por el valor de las IPs internas de su red. Una vez encontrada la IP, un nuevo escaneo de puertos me dejó descolocado: no había absolutamente ninguno, todos estaban cerrados a cal y canto.

Decidí que poco más podría hacer sin instalar la aplicación y esnifar los datos que intercambiaba con el aspirador, pero no me hacía ninguna gracia meterla en mi móvil, así que cogí una vieja tablet que no usaba para nada, la formateé, la registré con una cuenta de correo creada ex profeso, y me fui a la Play Store a instalar la app

Y no estaba.

Probé con todo tipo de combinaciones, pero aunque estaban algunas otras apps de Cecotec, la que había visto para la Conga 1490 y 1590 no aparecía por ninguna parte. Curiosamente fue lo mismo que me ocurrió cuando probé con un Android X86 corriendo en una máquina virtual… Mi conclusión es que, dado que en el manifiesto de la app pide permitir hacer llamadas, la Play Store sólo deja instalarla en dispositivos con tarjeta SIM.

Sin embargo, si nos bajamos el APK desde otra fuente, lo copiamos a la tablet y lo instalamos (tras permitir la instalación desde fuentes no confiables, claro) la aplicación funciona perfectamente.

Con ella ya instalada, llega la segunda parte: configurar un punto de acceso que nos permita esnifar todo el tráfico. El problema surge cuando nos damos cuenta de que es muy difícil hacer eso en WiFi, así que mi solución fue usar una Raspberry Pi configurada como punto de acceso, y correr en ella tcpdump para capturar todo el tráfico que pasase.

Para configurar una Raspberry Pi como punto de acceso basta con seguir estas instrucciones:

https://www.raspberrypi.org/documentation/configuration/wireless/access-point-routed.md

Yo utilicé la res 192.168.18.X para la WiFi, y así asegurarme de que no haya interferencias con nada más. Luego, para capturar todo el tráfico, uso el comando:

sudo nohup tcpdump -ni wlan0 '(not dst 192.168.18.4) and (not src 192.168.18.4)' -s0 -w datos.pcap

Siendo 192.168.18.4 la dirección IP de mi ordenador de sobremesa, para que no capture los paquetes que se intercambian cuando entro por SSH, y así tener una captura limpia. El parámetro w almacena los paquetes en bruto en el fichero datos.pcap, lo que nos permite luego abrirlo en, por ejemplo, WireShark, para analizarlos con más detenimiento.

Y con esto está todo listo para empezar, así que puse en marcha la captura de paquetes, y en la tablet lancé la aplicación, rellené los campos con una dirección de correo nueva, creada para la ocasión, emparejé la aspiradora, y lancé un escaneo de puertos… ¡y ahora la aspiradora tenía uno abierto, el 8888!

Buena cosa, pues, pero ahora tocaba parar la captura y empezar a analizar qué habían hecho la aspiradora y la app.

Una vez copiado el fichero con el volcado, procedí a abrirlo con el Wireshark. Lo primero que hace la aspiradora, como cabe esperar, es obtener una IP, pero luego resulta que lo siguiente que hace es conectarse a Ibl-app-eu.robotbona.com, y enviar una petición POST a baole-web/common/sumbitClearTime.do con un formulario con el siguiente contenido (en realidad va en formato URLEncode:

appKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
deviceId=yyyyyyyyyyyyyyy
deviceType=1
clearTime=0

Donde xxxxxxx e yyyyyyy son unas ristras de números hexadecimales que, probablemente, identifiquen a mi aspiradora. La respuesta es un OK con el siguiente texto:

2b
{"msg":"ok","result":"0","version":"1.0.0"}
0

Que resulta ser un bloque en formato chunked con un resultado en formato JSON. A continuación otra conexión al mismo servidor, pero esta vez pidiendo:

appKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
authCode=zzzzz
deviceId=yyyyyyyyyyyyyy
deviceType=1
funDefine=11101
nonce_str=AAAAA
version={"wifi":"1.0.48","mcu":"3.9.1714(513)"}
sign=SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS

Una vez más lo he anonimizado just in case. La respuesta es la misma que antes:

2b
{"msg":"ok","result":"0","version":"1.0.0"}
0

Hasta aquí la aspiradora se ha conectado al puerto 80 del destino, lo que es consistente con el uso de REST. Sin embargo, ahora la aspiradora se conecta al puerto 20008 del destino, y comienza un intercambio de mensajes diferente. La siguiente petición desde la aspiradora es un bloque que comienza con una secuencia de 20 bytes binarios, seguidos por 349 bytes en ASCII en formato JSON (el JSON, en realidad, se transmite en una única línea y sin formato, pero lo he embellecido para mejorar la lectura. Si en algún punto incluyese un LF, incluiría el símbolo \n):

71 01 00 00 | 10 00 00 00 | 01 00 00 00 | 02 00 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"0",
    "targetType":"6",
    "broadcast":"0"
  },
  "value":{
    "token":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "deviceId":"yyyyyyyyyyyyyy",
    "appKey":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "deviceType":"1",
    "authCode":"zzzzz",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "firmwareVer":"{\"wifi\":\"1.0.48\",\"mcu\":\"3.9.1714(513)\"}"
  }
}

Aquí ya empieza a haber cosas interesantes: la aspiradora comunica al servidor cual es la IP interna y el puerto que ha abierto para comunicarse. Estos datos son importantes para la app. Se repiten los campos deviceId, appKey y authCode de mensajes anteriores, pero ahora se añade un token.

La respuesta también es complicada. En este caso es de 99 bytes, y comienza también con una ristra de 20 valores binarios, seguido de 79 bytes con un JSON normal:

63 00 00 00 | 11 00 c8 00 | 01 00 00 00 | 02 00 00 00 | 00 00 00 00
{
   "msg":"login succeed",
   "result":0,
   "version":"1.0",
   "time":"2020-06-21-04-59-39"
}

En el primer mensaje, los dos primeros bytes en formato little endian forman el número 369, que coincide con la suma de los 20 bytes binarios y los 349 bytes de JSON. En el segundo mensaje, los dos primeros bytes coincide con 99 en decimal, por lo que parece que, como mínimo, los dos primeros bytes indican el tamaño de la respuesta, probablemente para poder reservar la cantidad de memoria que necesite en el momento, y no tener que reservar de más, o ampliando bloques. ¿Qué significan el resto de bytes? De momento no tenemos información suficiente para saberlo.

Siguiente envío desde la aspiradora 444 bytes:

bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 03 00 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control": {
    "targetId":"0",
    "targetType":"6",
    "broadcast":"0"
  },
  "value": {
    "noteCmd":"102",
    "workState":"6",
    "workMode":"0",
    "fan":"1",
    "direction":"0",
    "brush":"2",
    "battery":"100",
    "voice":"2",
    "error":"0",
    "standbyMode":"1",
    "waterTank":"40",
    "clearComponent":"0",
    "waterMark":"0",
    "version":"3.9.1714(513)",
    "attract":"0",
    "deviceIp":"192.168.18.14",
    "devicePort":"8888",
    "cleanGoon":"2",
    "extParam":"{\"cleanModule\":\"3\"}"
  }
}

Y otra vez tenemos una mezcla binaria y ASCII, en este caso de 60 bytes:

3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 03 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

Esto nos confirma que, efectivamente, lo primero que recibimos es el tamaño de la respuesta completa.

Editado: faltaba un \n al final del JSON.

Ahora la aspiradora envía sólo 20 bytes, lo que es sólo la cabecera:

14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 04 00 00 00 | e7 03 00 00

Y la respuesta del servidor es

14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 04 00 00 00 | e7 03 00 00

En este punto, la app se conecta a la aspiradora y empieza a intercambiar datos con este mismo formato, pero de momento vamos a dejar esa parte de lado, pues esta conexión con el servidor remoto sigue estando activa. De hecho, 30 segundos después del envío anterior, la aspiradora envía de nuevo otra secuencia, en concreto:

14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 05 00 00 00 | e7 03 00 00

Y la respuesta del servidor es

14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 05 00 00 00 | e7 03 00 00

Otra vez inactividad hasta que 30 segundos después volvemos a tener otro paquete desde la aspiradora:

14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 06 00 00 00 | e7 03 00 00

Y la respuesta del servidor es

14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 06 00 00 00 | e7 03 00 00

Lo interesante es que empieza a haber un patrón aquí. Si asumimos que son cinco números de cuatro bytes cada uno, el cuarto número es un número de secuencia: en efecto, comienza en 2 en la primera petición, y su respuesta tiene el 2 también. La siguiente petición tiene el valor 3, y su respuesta también. Luego viene el 4, y otra vez la respuesta, el 5 y otra vez la respuesta, el 6 y su respuesta… Además, el resto de los números de las peticiones y respuestas 4, 5 y 6 son iguales, lo que hace suponer que se utilizan para mantener la conexión activa. Por otro lado, en las peticiones, el quinto byte tiene el bit 0 a cero, y en las respuestas están a uno. En el sexto byte, además, el bit 0 está a cero en peticiones con datos, y a 1 en las que son sólo para mantener la conexión.

Veamos ahora qué pasa con la app en sí. Como ya dije, se conecta al puerto 8888, pero lo raro es que establece dos conexiones en vez de una. ¿Por qué? Ni idea. Pero veamos con calma qué hace.

En la primera conexión tenemos este intercambio, antes de que la aspiradora la cierre (t es tablet, a es aspiradora):

t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00

En la segunda conexión se intercambian estos datos:

t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00

O sea, más o menos lo mismo. De aquí no puedo sacar mucha información.

Hasta aquí la primera parte. Ahora toca empezar a hacer pruebas para intentar descubrir cómo dar órdenes a la aspiradora.

Parte 2

Compilando Gnome Shell

Estos últimos meses he estado preparando un parche para Mutter, y he necesitado instalar la rama de desarrollo de Gnome Shell. Y aunque hay bastante documentación, hay detalles que están algo incompletos, así que voy a poner aquí lo que he ido aprendiendo.

Para empezar, toda la compilación se hace mediante jhbuild, lo que simplifica muchísimo el trabajo. Y aunque es mucho más sencillo que bajarse a mano todo, aún tiene algunos detalles peliagudos.

Lo primero, si queréis tener éxito en compilar una versión de desarrollo, es mejor que utilicéis Fedora, en concreto la versión de desarrollo (o beta) también. De esta manera no tendréis problemas de versiones con las dependencias. Para ello, nada mejor que utilizar una máquina virtual. Aunque VirtualBox es una buena opción, yo tuve serios problemas con los controladores de pantalla (y es raro… con Debian o Ubuntu siempre fueron perfectamente), por lo que al final me decanté por Gnome Boxes, una herramienta más sencilla, pero con la que no tuve ningún problema.

Una vez instalada, bajamos jhbuild y lo instalamos con:

git clone https://gitlab.gnome.org/GNOME/jhbuild.git
cd jhbuild
./autogen.sh
make
make install

Una vez hecho esto debemos añadir (si no lo tenemos ya) a nuestro PATH el directorio ~/.local/bin. Y ya tendremos jhbuild instalado.

El siguiente paso es pedir que nos construya Gnome Shell. Para ello escribimos:

jhbuild build gnome-shell

Sin embargo, lo más probable es que no funcione y nos diga que nos faltan un montón de dependencias. Para instalarlas, basta con ejecutar:

jhbuild sysdeps – -install gnome-shell

y tomarse un café (o dos), porque tarda un buen rato en bajar e instalar las dependencias. Cuando termine volvemos a ejecutar el comando previo, y probablemente nos dirá que nos falta aún libavcodec y compañía. Por suerte es posible compilarlo aún así, mediante:

jhbuild build – -nodeps gnome-shell

Ahí empezará a compilar por fin, y tardará, tardará mucho (y requerirá mucho disco duro, por cierto. Aseguraos de que vuestra máquina virtual tiene suficiente). Pero lo importante que debemos saber es que el código lo baja en ~/jhbuild/checkout, por lo que si queremos aplicar parches, es ahí donde hay que tocar el código.

Cuando termina de compilar, instala todo en ~/jhbuild/install, y ya podemos probarlo. Yo lo que suelo hacer es cerrar la sesión gráfica, irme a una consola de texto (con Alt+F2), y ahí ejecutar primero:

jhbuild shell

para tener una shell dentro del entorno, y luego ejecutar:

dbus-run-session – – ~/jhbuild/install/bin/gnome-shell – -wayland – -display-server

y se arranca la sesión. Un problema que yo no he conseguido resolver es que la opción de Cerrar sesión no funciona. Yo lo resuelvo abriendo una terminal y matando el proceso.

Creando un pincho USB de instalación de Windows

A veces no nos queda otra que usar Windows. Hay muchas circunstancias, pero podemos resumirlas en:

  • tenemos un programa que sólo funciona en Windows, y Wine no es una opción.
  • Alguien nos pide que le (re)instalemos el Windows en su equipo.

Aunque los fabricantes suelen incluir la opción de restaurar el sistema a los valores de fábrica, en mi opinión es mejor instalar la versión oficial, pues no trae mucha de la basura que meten los fabricantes. Para ello podemos bajar la ISO oficial desde la página de Microsoft. No hay problema por que nuestro ordenador no tenga una pegatina con un número de activación, pues éste se guarda en la BIOS. Es más: si no tenemos licencia, Windows 10 funciona exactamente igual, y la única limitación es que muestra un pequeño mensaje en la esquina inferior derecha pidiendo que te registres, y que no permite personalizarlo (cambiar colores, el fondo de escritorio…).

El problema viene cuando nos encontramos con que muchos equipos actuales no traen unidad de DVD, con lo que, o no podemos grabar la imagen ISO. La solución en ese caso consiste en crear un pincho USB para arrancarlo. Y aunque hay muchas utilidades para ello, en realidad es mucho más sencillo de lo que parece: basta con coger un pincho formateado en FAT32, copiar dentro todos los ficheros que hay dentro de la imagen ISO, y listo: ya tenemos un pincho arrancable en cualquier sistema UEFI… al menos hasta que descubrimos que la última versión de la ISO de Windows tiene un fichero que ocupa más de 4GB, y por tanto no se puede copiar en una partición FAT32.

Aunque algunas BIOS pueden arrancar también desde particiones NTFS, no todas lo permiten. Pero, afortunadamente, hay una solución, apuntada por win10guru:

  • Creamos dos particiones en el pincho USB: la primera, de unos 5GB, en formato FAT32, y la segunda en formato NTFS.
  • Copiamos en la primera todas las carpetas de la ISO excepto la de Sources.
  • Creamos una carpeta Sources vacía en la partición FAT32 y copiamos dentro únicamente el fichero boot.wim, que está dentro de la carpeta Sources de la imagen ISO.
  • Por último, copiamos todos los ficheros y carpetas de la ISO en la partición NTFS.

De esta manera, la BIOS arrancará desde la partición FAT32, y cederá el control a la partición NTFS.

Una vez instalado, conviene eliminar la basura que mete Microsoft. Hay varias opciones: la primera es eliminar Windows Search y Superfetch. La segunda, utilizar uno de los muchos scripts que eliminan programas que no se utilizan, tales como:

Por supuesto, yo no soy responsable de lo que hagan estos scripts, así que cada uno debe revisarlos antes para ver si lo que hacen es lo que se espera.

Desktop icons, Gnome shell y Wayland

Hace cosa de un año y medio la gente de Gnome anunció que eliminaba de Nautilus todo el código que pintaba los iconos de escritorio. Para los que no se quieran leer el tocho, resumiré rápidamente que el motivo era, básicamente, que dicho código venía de muchos años atrás, era extremadamente complejo debido a las limitaciones de las versiones antiguas de GTK, y cada vez era más difícil de mantener a la vez que se añadían nuevas características al resto de Nautilus. Además, hacía ya seis años que Gnome 3 no utilizaba iconos de escritorio. Por si fuera poco, tenía muchos bugs de difícil solución (por ejemplo, el soporte multimonitor no funcionaba demasiado bien). Y a todo esto había que sumar el hecho de que en Wayland no funcionaría correctamente debido a las limitaciones que impone en aras de la seguridad: en efecto, en Wayland una aplicación no puede decidir donde colocar una ventana ni mantenerla fija en el fondo, entre otras cosas. El motivo de esta limitación es evitar que una aplicación maliciosa pueda hacerse pasar, por ejemplo, por el escritorio, o por una barra de tareas, etc, poniendo en riesgo la seguridad del sistema. Por desgracia, esto también significa que, en Wayland, cosas como las barras del escritorio, un dock o los iconos de escritorio no se pueden delegar en una aplicación, sino que tienen que ser manejadas desde dentro del gestor de ventanas. Es por esto que se creó la extensión de Gnome Shell llamada Desktop Icons: para seguir ofreciendo iconos en el escritorio en Gnome Shell para aquellos que lo deseasen (como yo).

La versión disponible en aquel momento ya implementaba la funcionalidad más básica, pero tenía un defecto que, para mí, era muy grave: no permitía utilizar una única pulsación para lanzar un icono, sino que obligaba a utilizar doble click. Como yo estoy acostumbrado a la primera manera, me lié la manta a la cabeza y envié un parche para implementarlo. Tras muchos cambios para adecuar el estilo de código al que se utiliza en el proyecto Gnome y más cosas (nunca podré agradecer lo suficiente a Carlos Soriano su infinita paciencia enseñándome a manejar Git en condiciones), lo aprobaron. Y le cogí el gustillo, con lo que detrás de él vino otro, y otro, y otro más… Hasta que, recientemente, me ofrecieron ser el mantenedor del código (lo que para mí es un honor, todo sea dicho).

Un año después, la extensión ya incorpora todo lo que se pretendía para la versión 1.0 y más, y se incluye por defecto en la versión de Gnome Shell de Ubuntu, y no puedo menos que agradecer a toda la gente que ha colaborado con parches e informes de bugs.

Por desgracia, a medida que más y más usuarios la han ido instalando, han ido surgiendo algunos problemas inherentes al hecho de que sea una extensión, problemas que no eran nada evidentes al principio y que sólo se han ido haciendo visibles a medida que la cantidad de usuarios crecía.

El primer gran problema es que todas las extensiones se ejecutan en el mismo bucle principal que el compositor de ventanas. Esto significa que una extensión que necesite mucho tiempo para ejecutar una operación puede, literalmente, congelar todo el escritorio, incluyendo el mismísimo repintado de todas las ventanas. En el caso de extensiones «normales», que se limiten a mostrar un icono en la barra de tareas o así, esto no es un problema, porque el trabajo que realizan es mínimo; sin embargo, en el caso de Desktop Icons sí lo es, pues cada vez que tiene que refrescar el escritorio (por ejemplo porque se añade o borra un fichero), tarda en torno a medio segundo en realizar todas las operaciones (leer la lista de ficheros en el directorio, obtener sus metadatos, generar los pixmaps, eliminar las cuadrículas e iconos previos, generar una nueva cuadrícula, crear los nuevos iconos, y pintarlos en su sitio), y durante ese tiempo todo el escritorio se congela. Obviamente, por que ocurra una vez cada mucho no es muy problemático, pero sin duda es molesto para el usuario.

Solucionar esto, aunque no es totalmente imposible, no resulta sencillo: actualmente ya utilizamos funciones asíncronas en todos los sitios posibles para evitar bloquear la cola de eventos, pero no es suficiente. Además sería necesario evitar repintar todo el escritorio y sus elementos, y sólo añadir o quitar los iconos de los ficheros que se hayan añadido o eliminado. Pese a todo, esto sólo reduciría un poco más el problema, pero no lo resolvería del todo, pues si un programa añade y borra constantemente un fichero al escritorio, por ejemplo, puede aún bloquear la cola, en buena medida porque algunas operaciones de repintado se realizan sólo cuando ya no quedan operaciones pendientes en la cola de eventos. Además, implementar esto implicaría un importante rediseño interno, y teniendo en cuenta que algunas distribuciones cuentan con Desktop Icons para sus versiones de soporte a largo plazo, no es algo que se pueda hacer de cualquier manera, sino que debe implementarse de manera muy progresiva y con una buena revisión por pares de todos y cada uno de los parches, para garantizar que no hay errores ni regresiones.

Otro problema es la imposibilidad (al menos actualmente) de integrar Drag’n’Drop al completo: aunque dentro de Gnome Shell existe soporte de Drag’n’Drop, no es compatible con operaciones desde el «espacio de usuario»; esto es: una aplicación no puede ni enviar al compositor, ni recibir de él, eventos de Drag’n’Drop . Aunque en principio sería posible implementar dicha comunicación, no es una tarea trivial, y de hecho, tras tantear a algún programador de Mutter, la conclusión es que es lo suficientemente complicado como para que sólo valga la pena pasar el trabajo de implementarlo en Wayland, pero no en X11.

Y de aquí llegamos al tercer problema: las extensiones se escriben con Clutter + St, los cuales no funcionan exactamente igual que Gtk. Un ejemplo es la forma en que se procesan los eventos de enter_notify y exit_notify, que obligó a añadir una serie de trucos en el código que permite seleccionar un grupo de iconos mediante «goma elástica», para gestionar correctamente aquellos casos en los que el cursor entraba en una ventana en mitad de una selección, o cuando pasaba por encima de la barra superior de Gnome. Esto es algo que en Gtk está resuelto desde hace años, gracias al mayor número de usuarios y programadores que trabajan con él, y que permite detectar más bugs.

El cuarto problema radica en los cambios entre versiones del escritorio. Dado que Gnome Shell no ofrece una API estable para las extensiones, éstas tienen que interactuar directamente con el código interno, por lo que cualquier cambio les puede afectar. En el caso de una extensión tan compleja como Desktop Icons este problema es aún mayor, hasta el punto de que la actual versión 19.01 será, probablemente, la última compatible con Gnome Shell 3.30 y 3.32, y las nuevas versiones necesitarán como mínimo Gnome Shell 3.34.

A la vista de todos estos problemas, hace un par de meses empecé a sopesar la posibilidad de hacer un cambio radical en el diseño y mover toda la lógica a un proceso independiente del compositor, de manera que toda la gestión de los iconos del escritorio se realice sin interferir con la composición y demás, además de utilizar Gtk directamente en lugar de St y Clutter. De hecho, cuando se decidió crear la extensión, los autores originales (Carlos Soriano y Ernestas Kulik) sopesaron seriamente esta misma posibilidad, pero lo descartaron precisamente por las limitaciones impuestas por el modelo de seguridad de Wayland, y porque hacerlo dentro de una extensión tenía más sentido en aquel momento, pues era muchísimo más fácil.

Por supuesto, esto es más sencillo de decir que de hacer, pues, como ya dije antes, aunque en X11 no hay ningún problema en que una aplicación mantenga una ventana en el fondo, o que la elimine de la lista de ventanas para que no aparezca al hacer Alt + Tab, en Wayland eso es totalmente imposible por diseño. Además, dado que las extensiones están escritas en Javascript, no es posible lanzar código en un nuevo thread, y aunque se pudiese, no sería posible que dicho código llamase a funciones de Clutter o St, por lo que hay que descartar la idea de descargar el trabajo en un thread paralelo. Lanzar un proceso independiente sí es posible, pero éste trabajaría desde fuera del compositor, por lo que, en principio, seguiríamos con el mismo problema.

Sin embargo, existe una alternativa extra, que es justo la que decidí investigar, que consiste en repartir el trabajo: por una parte tenemos un programa normal, que trabaja desde fuera del compositor y que utiliza GTK para gestionar absolutamente todo lo relacionado con el escritorio y sus iconos mediante una o varias ventanas normales, exactamente igual a como hacía el viejo Nautilus en el modo «iconos de escritorio»; y, por otro lado, una pequeña extensión cuyo trabajo consiste en lanzar el programa anterior (y relanzarlo cada vez que se muera), detectar la ventana que abre, y asegurarse de que ésta se mantenga donde debe. De esta manera, el código dentro de la extensión es mínimo y se limita exclusivamente a las operaciones que son imposibles de realizar desde el exterior del código del compositor.

Por supuesto, es fundamental no romper el modelo de seguridad de Wayland, lo que significa que no se puede permitir bajo ningún concepto que una aplicación extraña se pueda aprovechar de este mecanismo para colocar su propia ventana como el fondo del escritorio (o como cualquier otro elemento). Para ello, la única solución es que sea la extensión quien lance el proceso, y que cada vez que aparezca una ventana, compruebe si ésta pertenece al proceso que ella misma ha lanzado, otorgándole esos privilegios exclusivamente en el caso de que así sea. Dado que el código ha sido lanzado específicamente por la extensión, se puede considerar que es código tan confiable como el de dicha extensión (y más si el programa a lanzar forma parte de la propia extensión): si un programa malicioso puede reemplazar el código de la aplicación de escritorio, también podría hacerlo directamente con el código de la extensión, por lo que el nivel de seguridad es exactamente el mismo.

Ahora llega la cuestión de cómo detectar que una ventana pertenece al proceso lanzado desde la extensión. Mi primera idea fue utilizar metawindow_get_pid() en cada ventana nueva que apareciese, para obtener el PID del proceso que creó la ventana y compararlo con el PID del proceso que hemos lanzado desde la extensión; por desgracia, dicha llamada sólo funciona en X11 pero no en Wayland, porque utiliza un dato específico de X11. Sin embargo, existe otra llamada, metawindow_get_client_pid(), que sí funciona desde ambos sistemas; por desgracia es privada, lo que significa que sólo se puede llamar desde C y desde dentro de mutter, nunca desde una extensión escrita en Javascript.

Propuse entonces en el canal IRC de Gnome Shell que dicha llamada se cambiase a pública, pero mi idea no convenció porque existen varios ataques que involucran PIDs de procesos, por lo que, aunque yo hiciese las cosas bien, hacer pública dicha llamada podría suponer abrir la caja de Pandora. Sin embargo, sí me redirigieron al código de XWayland para que viese ahí como lo hacen de manera segura: básicamente, al lanzar el proceso crean manualmente un socket y asignan un extremo a Wayland, pasando el otro extremo al proceso hijo para que se comunique a través de él; luego basta con engancharnos a la señal map() y, por cada ventana que aparezca, comparar si su socket coincide con el que creamos nosotros para nuestro proceso hijo, en cuyo caso podemos estar seguros de que esa ventana pertenece a él y no a otro.

La idea era buena, pero por desgracia no se puede implementar directamente en Javascript porque precisa de varias llamadas privadas de mutter, además de que tampoco se pueden crear sockets desde Javascript. Ante esto me sugirieron escribir una clase GObject que lo implementase y exportase una interfaz para ello, y así lo hice: mi primer parche para mutter y mi primera clase GObject. Esta clase sólo funciona con Wayland (para X11 no tiene sentido, pues la propia aplicación puede detectarlo y hacer ella misma el trabajo), y permite lanzar un proceso y detectar si una ventana concreta pertenece o no a dicho proceso. Si se utiliza desde X11, la parte de lanzar el proceso también funciona, pero el método de detectar si una ventana pertenece o no al proceso genera una excepción (que, obviamente, se puede capturar). Esto permite simplificar el código en la extensión a la hora de hacer que funcione en ambos entornos de ventanas.

Aunque dicho parche funciona bien, aún sigue pendiente de aprobación; pero yo quería poder usar YA la nueva versión de Desktop Icons, así que, como a cabezón no me gana nadie, decidí ver qué podía hacer para detectar de manera segura la ventana de un proceso utilizando sólo lo que ya tenía disponible en Javascript. Para ello se me ocurrió que la aplicación de escritorio podría poner como título de la ventana una cadena concreta que la extensión pudiese identificar (tiene que ser en el título, pues no parece haber absolutamente nada más en una ventana que se pueda asignar de manera libre por el programa y que una extensión pueda leer). El problema es que dicha cadena no puede ser predecible, pues entonces otras aplicaciones podrían hacerse pasar por la legítima. Ante esto, decidí que la extensión generaría un UUID aleatorio de 128 bits justo antes de lanzar el proceso, y se lo pasaría a éste para que lo ponga en el título de la ventana (y, por supuesto, calculando uno nuevo cada vez que la aplicación se muera). Pero claro, pasarlo como parámetro por la línea de comandos sería completamente inseguro porque cualquier programa puede leer los parámetros de cualquier otro proceso (basta hacer un ps ax o leer /proc), por lo que tenía que ser algo más seguro. Al final la solución consistió en pasarlo a través de stdin, de manera que nadie más pueda leerlo (habría sido más elegante utilizar un pipe específico, pero por desgracia desde Javascript no es posible crear nuevos pipes).

Para simplificar aún más el código escribí una pequeña clase Javascript que es compatible a nivel de métodos con la clase GObject de mi parche, de manera que, si se aprueba, sólo tendré que reemplazar una clase por otra en el código (o incluso utilizar una u otra en función de la versión de Gnome Shell).

Con esto resolví el primer problema, pero ahora quedaba el segundo: aunque ya puedo identificar qué ventana es la del escritorio ¿como hago para mantenerla donde debe estar?

La solución elegante sería cambiar el tipo de ventana a uno de los tipos estándar (en concreto, a META_WINDOW_DESKTOP). Por desgracia, desde Javascript no es posible cambiarlo, pues la llamada es privada. Obviamente preparé un parche para cambiarlo, donde, además de hacerla pública, también la renombro para que sea consistente. Este cambio convenció mucho más en el canal IRC, por lo que espero que sea finalmente aceptado junto con el otro, pues la ventaja de estos dos parches es que no son específicos para este proyecto, sino que permiten, en general, «externalizar» el trabajo que actualmente se realiza dentro del compositor, lo que permitiría que más elementos, como por ejemplo la barra superior o un dock, puedan ser gestionados desde un proceso externo.

Sin embargo, seguía queriendo poder utilizar YA el programa, así que lo que hice fue engancharme a varias señales para forzar la ventana a permanecer en su sitio:

  • raised: esta señal se emite cada vez que una ventana pasa a primer plano. En su callback llamo al método lower(), que se encarga de mandarla debajo de todas las demás ventanas, manteniéndola así al fondo.
  • position-changed: como su nombre indica, esta señal se emite cuando el usuario cambia la posición de una ventana. En el callback la devuelvo siempre a donde le corresponde. Y es que, aunque la ventana del escritorio no está decorada (y, por tanto, en principio el usuario no tendría donde pinchar para moverla), sigue siendo posible utilizar combinaciones de teclas para cambiarla de sitio, cosa que no se puede permitir.

A mayores llamo a stick() para que la ventana aparezca en todos los workspaces.

Con esto ya se puede conseguir que la ventana permanezca siempre en su sitio, pero aún queda por evitar que aparezca en la lista de ventanas. De no hacerlo, aparecerá en el modo Actividades de Gnome Shell y en el cambiador de ventanas (el de Alt + Tab). Para solucionar esto hay que reemplazar tres métodos:

  • Meta.Display.get_tab_list()
  • Shell.Global.get_window_actors()
  • Meta.Workspace.list_windows()

Con estos tres métodos, la ventana desaparece «lo suficiente» como para que sea usable (por ejemplo, en Dash to Dock no desaparece, pero es un mal menor). Por supuesto no es muy elegante, y el resultado será perfecto si se acepta el parche para cambiar el tipo de ventana.

Y de esta manera tan horrorosa (aunque sólo hasta que aprueben mis parches… si es que los aprueban, claro) conseguí mover toda la funcionalidad del escritorio fuera del compositor, lo que resuelve de un plumazo todos los problemas anteriores. El resto del trabajo fue, básicamente, convertir los widgets de St y Clutter en los equivalentes de Gtk, quitar mucho código asíncrono que ya no era necesario y sólo complicaba terriblemente la lógica, y algunos detalles a mayores como añadir código extra en la extensión para que, durante el arranque, le comunique cuantos monitores hay, así como sus coordenadas y tamaños.

La extensión ya está disponible en la página de extensiones de Gnome Shell, y ofrece, además de todo lo que ya tiene la extensión original, Drag’n’Drop, no congelar la composición del entorno gráfico cuando se refresca el escritorio, más velocidad, mostrar los nombres de ficheros demasiado largos cuando se pase por encima el ratón, y más.

Ventana transparente en Gtk 3 y Javascript

Hacer una ventana con el fondo transparente utilizando Gtk es un clásico; de hecho hay ejemplos de como hacerlo en python, en C y en Vala. Pero falta como hacerlo con Javascript, y dado que es el lenguaje de moda para Gnome (y también por una serie de circunstancias extra) me he decidido a hacer el ejemplo. Este es el código:

#!/usr/bin/env gjs

imports.gi.versions.Gtk = '3.0';
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;

Gtk.init(null);

let window = new Gtk.Window();
window.set_app_paintable(true);
let screen = window.get_screen();
let visual = screen.get_rgba_visual();
if (visual && screen.is_composited()) {
    window.set_visual(visual);
    window.connect('draw', (widget, cr) => {
        Gdk.cairo_set_source_rgba(cr, new Gdk.RGBA({red: 0.0,
                                                    green: 0.0,
                                                    blue: 0.0,
                                                    alpha: 0.0}));
        cr.paint();
        return false;
    });
    window.connect('delete-event', () => {
        Gtk.main_quit();
    });
    window.show_all();
    Gtk.main();
} else {
    print("El entorno de ventanas no admite transparencia");
}

Aunque el código no tiene nada de especial comparado con las versiones en otros lenguajes, sí es cierto que tuve problemas con Gdk.cairo_set_source_rgba, pues, como se ve, no sigue la estructura «normal» de llamadas como si fuera un objeto, sino que sigue el formato de C. Lo lógico sería hacer cr.cairo_set_source_rgba(…), pero no funciona. Y lo mismo con algunas otras.

Cargando mis cascos (y 2)

Hace ya dos años modifiqué unos cascos BlueTooth para que sólo con colgarlos de un soporte se cargase su batería. Aunque funcionó bien, en este tiempo hice algunos cambios extra, y creo que ha llegado la hora de publicarlos.

Para empezar, en el artículo anterior comentaba que había utilizado malla de desoldar para hacer los contactos. Aunque funcionó bien durante unos cuantos meses, lo cierto es que el pegamento de la cinta de doble cara fue permeando a través de la malla de cobre y acabó por dar problemas de conductividad, por eso al final opté por comprar cinta adhesiva de cobre y crear con ella los nuevos contactos (pulsa sobre las imágenes para verlas en grande).

Lo primero fue pegar la cinta por la parte de arriba de la diadema, como se ve en la foto, dejando pegada sólo aproximadamente un tercio. Luego, con unas tijeras, procedí a dar cortes en la parte que quedaba colgando, cada dos centímetros aproximadamente. De esta manera pude doblarlo sobre el lateral y garantizar que quedase bien pegado y, a la vez, con continuidad eléctrica sobre toda la superficie. Este fue el resultado:

El último paso en los cascos fue soldar los cables que vienen del puente de diodos a los dos contactos (véase la entrada anterior donde explico las modificaciones a nivel eléctrico), y proteger todo con un par de vueltas de cinta aislante en cada lado.

El siguiente punto era el soporte. En el artículo anterior mostré la chapuza que hice con una alcayata y lengüetas sacadas de una pila de petaca, pero llevaba tiempo con ganas de hacer algo más serio, así que decidí darle uso a las impresoras 3D de A Industriosa para construir un soporte en condiciones. Para ello, primero hice el diseño con FreeCAD, que tiene esta pinta y cuyo fichero podéis descargar desde este enlace.

Una vez impresa llegó la cuestión de como hacer las lengüetas para los contactos. Para ello decidí hacerlas de plástico flexible, sacado de una tapa de tornillos, y luego recubrirlas con cinta adhesiva de cobre. En la foto siguiente se ven las dos lengüetas.

Tras cortarlas, las pegué en el soporte utilizando cola térmica, quedando como en la siguiente foto.

El siguiente paso fue recubrirlas con la cinta adhesiva de cobre y soldar los cables del transformador de alimentación. Sin embargo, aunque quedó bien, es cierto que habría sido mejor pegar primero la cinta adhesiva y soldar los cables, y sólo después pegar las lengüetas con la cola térmica, pues cuando apliqué el soldador me encontré con que se me despegaban.

Tras hacer todo esto, éste es el resultado del soporte, listo para acoger en su seno a los cascos. Vemos que está fijado a la balda con un tornillo, y que los cables de alimentación están soldados en la parte superior, dejando toda la superficie de las lengüetas para que haga contacto con los electrodos de los cascos.

Y este es el resultado final: un soporte con mucha mejor presencia y calidad, y que me garantiza que mis cascos siempre estarán adecuadamente cargados cada vez que los necesite.


Presentando CRUST

Acabo de lanzar CRUST. Se trata de un analizador estático de C que permite disponer en C de una gestión de memoria similar a la de RUST.

Y es que RUST está de moda, pues al ofrecer seguridad en el acceso a memoria dinámica pero sin necesidad de un runtime (como un recolector de basura) o de otras técnicas (como el conteo de referencias), permite exprimir al máximo el rendimiento. El problema es que RUST es un lenguaje nuevo, con su sintaxis propia (que, además, diverge de la de C u otros lenguajes bastante), y que, por tanto, tiene una curva de aprendizaje.

Por otro lado, existen casos en los que no se puede utilizar (todavía) RUST, como el de un microcontrolador PIC, Atmel…, pues hace falta un compilador específico. En otros microcontroladores, como los basados en ARM, es posible utilizarlo, pero sigue teniendo el problema de que no es un compilador oficial, y por tanto hay que hacer algún que otro malabar para integrarlo en la toolchain del fabricante.

Es aquí donde CRUST hace su aparición: como ya dije se trata de un analizador estático de C que permite disponer de (más o menos, claro) las mismas comprobaciones de seguridad que ofrece RUST para la gestión de memoria dinámica, de manera que es más difícil que un programa sufra referencias colgantes o dangling pointers, o pérdidas de memoria.

A la hora de diseñar CRUST tenía una cosa muy clara en mente: no podía crear un nuevo lenguaje parecido a C, sino que tenía que seguir siendo C puro, compilable con absolutamente cualquier compilador estándar. Eso eliminaba cualquier tipo de preprocesador del estilo de Metaobject o similares. También suponía rechazar cualquier tipo de conjunto de macros que pudiese alterar el código de la más mínima manera. Y por supuesto, el uso de bibliotecas estaba completamente descartado.

La solución consistió en crear una serie de calificadores específicos, similares en funcionamiento a los calificadores volatile o const ya disponibles en C, que permitan al analizador saber si un puntero concreto es gestionado o no-gestionado, así como otras propiedades importantes para el analizador. Estos calificadores comienzan todos con el prefijo __crust_ para evitar interferencias con nombres de variables o futuras adiciones al lenguaje C. La clave de estos calificadores es que no son necesarios en absoluto para compilar el código.

Por supuesto, ningún compilador aceptaría un código con dichos calificadores, y por eso es necesario incluir un fichero de cabecera (que se incluye con el analizador estático) que define dichos nuevos calificadores como espacios en blanco para el preprocesador de C. De esta manera, a la hora de compilar estos calificadores simplemente «desaparecen», y sólo son tenidos en cuenta cuando se utiliza el analizador. Este es un trozo de dicho fichero de cabecera, para que se entienda mejor:

#ifndef ENABLE_CRUST_TAGS

#ifndef __crust__
#define __crust__
#endif

#ifndef __crust_borrow__
#define __crust_borrow__
#endif

#ifndef __crust_recycle__
#define __crust_recycle__
#endif

#ifndef __crust_alias__
#define __crust_alias__
#endif

#ifndef __crust_no_0__
#define __crust_no_0__
#endif

...

Como se ve, se define cada posible calificador como una cadena vacía, lo que hace que el preprocesador se encargue de limpiar el código y dejarlo listo para el compilador, sin necesidad de modificar nada. Esto permite programar como de costumbre, simplemente etiquetando aquellos punteros que deben ser gestionados como un bloque CRUST, y compilando el código normalmente con el toolchain habitual, y sólo de vez en cuando pasar el analizador estático para comprobar si hemos cometido algún error al liberar o utilizar uno de estos bloques. Por supuesto, no es necesario escribir este fichero a mano, sino que se puede generar automáticamente simplemente llamando al analizador estático con el comando crust –headers, con lo que generará dicho fichero en el directorio actual.

La base de las reglas de gestión de memoria de CRUST (y, por extensión, de RUST) es que cada función es responsable de todos los bloques de memoria que genera o recibe. Así, si una función pide un bloque de memoria dinámica (por ejemplo con malloc), es su responsabilidad liberarlo o asegurarse que sea liberado. Esto puede ocurrir de tres maneras diferentes:

  • Puede liberar el bloque directamente ella misma
  • Puede llamar a otra función pasando dicho bloque como un parámetro, de manera que pase a ser responsabilidad de la nueva función garantizar que se libere dicho bloque
  • Puede devolver el bloque a la función llamante, de manera que ésta recibe la responsabilidad de liberarlo

No hay mucho más. Por supuesto existen, a mayores, otros detalles que hacen que la cosa no sea tan sencilla, por lo que para una explicación más en profundidad recomiendo leer como es el modelo de memoria de RUST.

Un ejemplo sencillo de como trabaja CRUST se puede ver en este trozo de código:

// SIEMPRE añadimos crust.h al principio
// El fichero tiene que estar en el proyecto
#include "crust.h"
#include <unistd.h>

// Definimos una estructura como "gestionada"
// simplemente añadiendo __crust__ a su definición
// Utilizamos un typedef para ahorrarnos tener que poner
// __crust__ en todos los sitios donde se utiliza
typedef __crust__ struct {
	int member;
	int p1;
	int p2;
} *un_tipo_t;

// esta función crea un nuevo bloque "gestionado" y lo devuelve

un_tipo_t funcion1();

// esta función recibe un bloque "gestionado",
// pero no lo libera antes de salir

void funcion2(un_tipo_t __crust_borrow__ parametro);

// esta función recibe un puntero a un bloque "gestionado",
// y además lo libera antes de salir

uint32_t funcion3(un_tipo_t parametro);

void main() {

	un_tipo_t bloque = funcion1();

	funcion2(bloque);
	funcion3(bloque);
}

Aquí vemos varias cosas:

  • Primero hacemos un typedef de un puntero a una estructura, y además incluimos el calificador __crust__. Esto significa que absolutamente cualquier variable de tipo un_tipo_t será gestionada, y por tanto sujeta a las reglas de CRUST.
  • Luego tenemos tres definiciones de funciones que «hacen cosas» con tipos un_tipo_t.
  • Finalmente, tenemos el bloque main. En él creamos un puntero de tipo un_tipo_t y le asignamos el bloque que nos devuelve funcion2.
  • A continuación llamamos con dicho bloque a funcion3. Como dicho parámetro está marcado como __crust_borrow__, sabemos que dicha función nunca liberará dicho bloque, por lo que después de llamarla seguirá estando disponible y podemos seguir utilizándolo.
  • Finalmente llamamos también con dicho bloque a funcion1. Como el parámetro de dicha función no está marcado como __crust_borrow__, sabemos a ciencia cierta que ese bloque que estamos pasando va a ser liberado dentro de ella, por lo que a partir de este punto no podemos volver a utilizarlo.
  • Llegamos al final de la función, y como la variable bloque ya no apunta a nada (pues el bloque fue liberado al llamar a funcion3), no hay riesgo de que tengamos una fuga de memoria.

Este código no devolvería ningún error al pasar por el analizador estático CRUST precisamente porque cumple con precisión las reglas de gestión de memoria. Sin embargo, si hiciésemos un cambio tan sencillo como invertir el orden de las llamadas a funcion2() y funcion3(), obtendríamos un error:

ERROR: Argument 1 when calling function 'funcion2' at line 41 was freed at line 40
Total: 1 errors.

El motivo es que funcion2() libera el bloque de memoria que recibe, lo que significa que cuando llamamos después a funcion3() con él, CRUST sabe que ese bloque de memoria ya no existe, y nos avisa.

Algo similar ocurre si sólo llamamos a funcion2() (que sabemos que no libera el bloque) pero no llamamos a funcion3():

ERROR: Memory block 'bloque', initialized at line 38, is still in use at exit point in line 41
Total: 1 errors.

Aquí CRUST se da cuenta de que el bloque que hemos inicializado no ha sido liberado al llegar al final de la función. Si lo dejásemos así tendríamos una fuga de memoria, y por eso nos avisa diligentemente.

Por supuesto CRUST es lo suficientemente inteligente como para seguir las posibles ramas de ejecución del código. Probemos a modificar la función main() anterior y dejémosla así:

void main() {
	// "tmp" tiene un valor que desconocemos
	uint8_t tmp;

	un_tipo_t bloque = funcion1();

	if (tmp == 5) {
		return;
	}

	if (tmp == 8) {
		bloque = NULL;
	}

	if (tmp == 7) {
		bloque = funcion1();
	}

	if (tmp != 3) {
		funcion3(bloque);
	}
	funcion2(bloque);
}

Al pasar este código a través de CRUST obtenemos el siguiente resultado:

ERROR: Memory block 'bloque', initialized at line 42, is still in use at exit point in line 45
ERROR: Assignment to 'bloque' at line 49, which was already assigned at line 42
ERROR: Argument 1 when calling function 'funcion2' at line 59 was freed at line 57
ERROR: Memory block 'bloque', initialized at line 53, is still in use at exit point in line 60
ERROR: Assignment to 'bloque' at line 53, which was already assigned at line 42
ERROR: Memory block 'bloque', initialized at line 42, is still in use at exit point in line 60
Total: 6 errors.

Aquí nos está avisando de todos los errores que hemos cometido, que son:

  • Si tmp vale 5 saldremos en el return de la primera comparación, con lo que el bloque que inicializamos en la línea 42 no se libera y tendremos una fuga de memoria.
  • Si tmp vale 7 u 8 estaremos sobreescribiendo un puntero que apunta a un bloque válido en la línea 49, con lo que tendremos una fuga de memoria.
  • Si tmp tiene un valor diferente de 3 liberaremos el bloque en la línea 49, con lo que al llamar a funcion2() tendremos una referencia colgante.
  • Si tmp vale 3 todo parecerá funcionar correctamente hasta llegar al final de la función, donde nos encontraremos con que el bloque nunca se libera y tendremos una fuga de memoria. Este error nos aparece dos veces porque en una de las ramas de ejecución no liberamos el bloque recibido al principio (línea 42) y en la otra no liberamos el bloque obtenido cuando tmp vale 7.

Por supuesto, CRUST tiene algunas limitaciones. Por ejemplo, sólo recuerda si una variable es NULL (vale 0) o no (valor distinto de 0), pero no valores concretos. Esto significa que este código será analizado correctamente:

void main() {

	un_tipo_t bloque = funcion1();

	if (bloque != NULL) {
		funcion3(bloque);
		bloque = NULL;
	}

	if (bloque != NULL) {
		funcion2(bloque);
	}
}

CRUST sabe que bloque, tal cual es devuelto por funcion1() puede ser NULL o no NULL, pero cuando llega al primer if y analiza ambas posibles ramas, en la de dentro del if marca a bloque como no NULL, y en la de fuera como NULL. Cuando llama a funcion3() el bloque es liberado, y por eso no devuelve un error al asignar NULL a dicha variable. A partir de aquí ambas ramas de ejecución tienen NULL como valor de bloque, y CRUST es capaz de detectar correctamente que jamás se llamará a funcion2(), y por eso no devuelve ningún error.

Sin embargo, este bloque sí daría errores, pues CRUST no llega a tener un nivel de control tan fino de los valores de las variables:

void main() {

	uint8_t tmp;

	un_tipo_t bloque = funcion1();

	if (tmp == 3) {
		funcion3(bloque);
	}

	if (tmp != 3) {
		funcion2(bloque);
		funcion3(bloque);
	}
}

Este código devolvería estos errores:

ERROR: Argument 1 when calling function 'funcion2' at line 48 was freed at line 44
ERROR: Argument 1 when calling function 'funcion3' at line 49 was freed at line 44
ERROR: Memory block 'bloque', initialized at line 41, is still in use at exit point in line 51
Total: 3 errors.

Por supuesto, la forma correcta de hacer lo anterior sería esta:

void main() {

	uint8_t tmp;

	un_tipo_t bloque = funcion1();

	if (tmp == 3) {
		funcion3(bloque);
	} else {
		funcion2(bloque);
		funcion3(bloque);
	}
}

La cual sí sería analizada correctamente por CRUST.

Todo esto no son más que unas pinceladas, pues hay mucho más en CRUST (por ejemplo el prestamo de bloques, igual que en RUST), por lo que lo mejor es leerse la documentación completa, que viene en formato PDF.

Como de costumbre, se puede encontrar en mi página web y en el respositorio de CRUST en GitLab.

Trust Flex Graphics Tablet

El otro día me compré una tableta gráfica Trust Flex Graphics Tablet. No es que suela dibujar a menudo, pero de vez en cuando me gusta hacer alguna cosa, y según qué tareas me dejan la muñeca fatal si las hago con el ratón.

Por desgracia ya al principio empezaron los problemas, pues Linux no me la detectaba. Sin embargo, lo raro era que sí había un dispositivo en /dev/input de la tableta, simplemente no se reconocía como un dispositivo digitalizador.

Empecé a rebuscar y encontré, por fin, gracias a una entrada del gitlab de freedesktop, que el problema se debe a que la tableta no entrega la resolución en unidades físicas; esto es, no se puede saber directamente a cuantos milímetros se corresponde unas coordenadas de posición. La solución consiste en añadir estas líneas en el fichero /etc/udev/hwdb.d/60-evdev.hwdb (creándolo si no existe):

#########################################
# Trust
#########################################

# Trust Flex Graphics Tablet
evdev:input:b0003v2179p0004*
 EVDEV_ABS_00=::234
 EVDEV_ABS_01=::328

Estas líneas añaden la entrada EVDEV_ABS_XX a los datos devueltos por el driver, de manera que libinput puede saber la resolución física de la tableta.

Una vez hecho esto hay que compilar el fichero mediante el comando

sudo udevadm hwdb --update

Y finalmente reiniciar para que el sistema aplique los cambios (sí, es necesario). Una vez hecho esto, la tableta funcionará perfectamente, apareciendo en pantalla un segundo cursor que seguirá al bolígrafo.

Más Gentoo para MipsEL

Estoy actualizando la distribución de Gentoo para webtv y, como no podía ser de otra manera, hay problemillas. El último ha sido con busybox. Para poder compilarla hay que añadir, además de las opciones que indico en Generando Gentoo para WebTV, hay que añadir las siguientes:

busybox_config_option n NANDWRITE
busybox_config_option n NANDDUMP
busybox_config_option n FLASH_ERASEALL
busybox_config_option n FLASHCP
busybox_config_option n BLKDISCARD

Por otro lado, la variable USE debe contener:

USE="${ARCH} -pam -fortran -sanitize -X -iptables -hardened -seccomp -ipv6 -systemd -mdev internal-glib -caps -gtk -qt -t -boehm-gc -nls -filecaps"