Usa la fuerza Luke: ejemplos de sun.misc.Unsafe

0
7843

En este tutorial vamos a ahondar en el oscuro mundo de la clase sun.misc.Unsafe de Java.

0. Índice de contenidos

1. Introducción

Java es un lenguaje de programación seguro y evita que el programador cometa varios errores basados en la gestión de memoria.
Sin embargo, existe una manera de cometer estos errores intencionadamente: usando la clase Unsafe.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.4 Ghz Intel Core I5, 8GB DDR3).
  • Sistema Operativo: Mac OS Yosemite 10.10.3
  • Entorno de desarrollo:
    • Eclipse Mars
    • Versión de Java: JDK 1.8.0_51

3. Instanciación

Antes de usarse, se debe crear una instancia del objeto Unsafe. Sin embargo, estamos frente a una tarea no trivial,
ya que el constructor de esta clase es privado.

Aunque hay varios métodos de crear esta instancia, usando reflexión el código queda muy limpio:

public static Unsafe getUnsafe() throws Exception {
  final Field f = Unsafe.class.getDeclaredField("theUnsafe");
  f.setAccessible(true);
  final Unsafe unsafe = (Unsafe)f.get(null);
  return unsafe;
}

Nota: Ignora tu IDE (Por ejemplo, en Eclipse sale un error de restricción de acceso, pero el código al ejecutarse funciona).
Si te molestan los mensajes de error, desactívalos para este tipo.

Nota 2: Aunque en el ejemplo se lanza la superclase Exception, en realidad se pueden dar dos tipos de excepciones (NoSuchFieldException, SecurityException).

4. API

La clase sun.misc.Unsafe se compone de 105 métodos, agrupados por manipulación de entidades.

Puedes ver la documentación
aquí.

Algunos de estos grupos con sus métodos más importantes son:

  • Info. Información de memoria a bajo nivel.
    • addressSize
    • pageSize
  • Objetos. Manipulación de objetos y campos.
    • allocateInstance
    • objectFieldOffset
  • Clases. Manipulación de clases y campos estáticos.
    • staticFieldOffset
    • defineClass
    • defineAnonymousClass
    • ensureClassInitialized
  • Arrays. Manipulación de arrays.
    • arrayBaseOffset
    • arrayIndexScale
  • Sincronización. Métodos a bajo nivel para sincronización.
    • monitorEnter
    • tryMonitorEnter
    • monitorExit
    • compareAndSwapInt
    • putOrderedInt
  • Memoria. Acceso directo a memoria.
    • allocateMemory
    • copyMemory
    • freeMemory
    • getAddress
    • getInt
    • putInt

5. Casos de Uso

Vamos a ver algunos casos de uso interesantes con esta Clase.

5.1. Evitar Inicialización.

El método allocateInstance puede resultar útil cuando queremos «saltarnos» la inicialización de un objeto o cuando se quiere
instanciar una clase que no dispone de constructor público.

Ahora creamos una clase cuyo constructor inicialice una variable. Utilizando el método del punto 3
nos saltaremos la inicalización del constructor.

Inicializator.java
package com.autentia.tutoriales.unsafe.initialization;

public class Initializator {

    private final long value; // no inicializada

    public Initializator() {
      this.value = 1; // inicialización
    }

    public void printValue() {
      System.out.println(this.value);
    }
}
public void testInicializator() throws Exception {

  final Initializator o1 = new Initializator(); // inicializa
  o1.printValue(); // 1

  final Initializator o2 = (Initializator)getUnsafe().allocateInstance(Initializator.class); // no inicializa
  o2.printValue(); // 0
}

El valor sin inicializar es un cero debido a que es el
valor por defecto de la clase long:

Mini-examen: ¿Qué les pasaría a los Singletons?

5.2. Corrupción de Memoria.

Vamos a considerar la siguiente clase que comprueba una regla de acceso:

AccessChecker.java
package com.autentia.tutoriales.unsafe.memoryCorruption;

public class AccessChecker {

    private int ACCESS_ALLOWED = 1;

    public boolean giveAccess() {
        return 42 == ACCESS_ALLOWED;
    }
}

Suponemos que el código cliente llama al método cada vez que se quiere acceder. Sin embargo, siempre retorna false. ¿Cómo «hackear»
el acceso?

La respuesta, de nuevo, nos la da Unsafe:

public void testMemoryCorruption() {
  final AccessChecker guard = new AccessChecker();
  System.out.println(guard.giveAccess()); // false

  try {
    corruptField(guard);
    System.out.println(guard.giveAccess()); // true
  } catch (final Exception e) {
    System.err.println(e.getMessage());
  }
}

private  void corruptField(AccessChecker guard) throws Exception {
  final Unsafe unsafe = getUnsafe();
  final Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
  unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // corrupción de memoria
}

Unas pequeñas notas:

  • Aunque se puede hacer por reflexión, de este modo vemos que se puede acceder a cualquier objeto,
    incluso sin referencias. Esto es así gracias al método objectFieldOffset

    • Por ejemplo, si tuvieramos otra instancia de AccessChecker en memoria a continuación
      de la actual, bastaría sumar el tamaño al offset (16 + unsafe.objectFieldOffset(f)). En ningún
      caso hemos hecho alguna referencia al objeto en cuestión.
    • 16 es el tamaño de AccessChecker en una arquitectura de 32 bits. Se puede calcular a mano o usar
      la función sizeOF, la cual definiremos en la próxima sección.
  • Si el atributo de la clase AccessChecker fuera final,
    no habría corrupción de memoria (Mini-examen: ¿Por qué?).

Nota importante: de nuevo, en el ejemplo se captura la superclase Exception, cuando se deberían capturar
las dos específicas del uso de Unsafe.

Nota importante 2: en el ejemplo se usa la variable guard como salida, además de ser una entrada. Esto es una mala práctica.
Aquí se usa con fines ilustrativos.

5.3. sizeOf.

Usando el método objectFieldOffset visto anteriormente se pude implementar una versión de
la conocida función de C sizeOf. Aunque hay formas mas sencillas de hacerlo, la idea clave
es la que sigue:

  1. Tomar la clase del objeto.
  2. Para cada campo no estático (incluyendo los de superclases):
    1. Obtener su offset
    2. Sumarlo al total
  3. Sumar el padding («huecos para que la dirección de memoria sea múltiplo de 8»)

Ya que no hemos puesto código, ahí va una aproximación
(¡OJO! ES PSEUDOCÓDIGO NO FUNCIONAL):

public long sizeOf(Object o){
  Class objectClass= o.getClass();
  List fields = new ArrayList<>();
  
  long size = 0;
  while(objectClass != Object.class){
    size += getFieldsSizeFromClass(objectClass.getDeclaredFields()));
    
    objectClass = objectClass.getSuperclass();
  }
  
  return addPadding(size);
}

private long getFieldsSizeFromClass(Field[] fields){
  Unsafe unsafe = getUnsafe();
  long maxSize = 0;
  
  for(Field f : fields){
    if(!f.isStatic()){
      long offset = unsafe.objectFieldOffset(f);
      if (offset > maxSize) {
        maxSize = offset;
      }
    }
  }
  return maxSize;
}

private long addPadding(long size){
  return ((size / 8) + 1) * 8;
}


De nuevo repetimos que no es código funcional, sólo es una aproximación a la implementación.
Ni siquiera es código java (usa funciones que no existen).

Para una manera segura y correcta de saber el tamaño de un objeto habría que usar el
paquete java.lang.instrument.

5.4. Copia superficial.

Una vez se dispone de una manera de saber el tamaño de un objeto, se puede implementar una función que
copie objetos (se asume que existe una implementación correcta de sizeOf):

public Object shallowCopy(Object obj){
  long size = sizeOf(obj);
  long start = objectToAddress(obj);
  
  long address = getUnsafe().allocateMemory(size);
  getUnsafe().copyMemory(start, address, size);
  
  return objectFromAddress(address);
}

public long objectToAddress(Object obj) {
  Object[] array = new Object[] {obj};
  
  long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
  
  return normalize(getUnsafe().getInt(array, baseOffset));
}

public Object objectFromAddress(long address) {
  Object[] array = new Object[] {null};
  
  long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
  getUnsafe().putLong(array, baseOffset, address);
  
  return array[0];
}

private static long normalize(int value) {
  if(value >= 0){
    return value;
  }
  
  return (~0L >>> 32) & value;
}

Básicamente toma el tamaño y la dirección de inicio del objeto, reserva memoria para el nuevo
y la rellena con una copia.

Las funciones objectToAddress y objectFromAddress se encargan de dar la dirección
de memoria de un objeto y el objeto que hay en una dirección de memoria, respectivamente.
La función normalize se encarga de «validar» que la dirección de memoria es positiva y
añadir el padding necesario;

Si se quiere copiar un objeto de manera correcta, sin tener que acceder a memoria directamente, se usa la interfaz
Cloneable.

5.5. Esconder Contraseñas.

Un uso muy interesante de la manipulación directa de memoria es el poder «eliminar objetos» de la memoria que no queremos. Por ejemplo,
contraseñas que hemos introducido en una aplicación.

La mayoría de funciones de manipulación de contraseñas tienen retorno de tipo byte[] o char[]. ¿Por qué arrays?

La respuesta es simple: seguridad. Se pueden ‘hacer null’ elementos de un array después de ser útiles. Sin embargo, las cadenas, aunque
las hagamos null, solamente se de-referencia el objeto, de modo que el objeto sigue en memoria hasta que el GC haga una pasada y limpie el entorno.

Usando reflexión se puede esconder la contraseña de manera segura:

public void testHidePasswordSafely() {
  String pwd = new String("l00k@myHor$e");
  
  System.out.println("\nForma segura de esconder contraseña");
  try {
    System.out.println("Original " + pwd);// l00k@myHor$e

    hidePasswordSafely(pwd);

    System.out.println("Escondida " + pwd); // ????????????
  } catch (final Exception e) {
    System.err.println(e.getMessage());
  }
}

public void hidePasswordSafely(String password) throws Exception {
  final Field stringValue = String.class.getDeclaredField("value");
  stringValue.setAccessible(true);
  final char[] mem = (char[])stringValue.get(password);
  for (int i = 0; i < mem.length; i++) {
    mem[i] = '?';
  }
}

Con la clase Unsafe también se puede conseguir, pero de manera algo menos segura:

public void testHidePasswordNotSafely() {
  String pwd = new String("1s4mazin6");
  
  System.out.println("Forma NO segura de esconder contraseña");
  try {
    System.out.println("Original " + pwd); // 1s4mazin6

    hidePasswordNotSafely(pwd);

    System.out.println("Escondida " + pwd); // ????????????
  } catch (final Exception e) {
    System.err.println(e.getMessage());
  }
}

public void hidePasswordNotSafely(String password) throws Exception {

  final String fake = new String(password.replaceAll(".", "?"));

  getUnsafe().copyMemory(objectToAddress(fake), objectToAddress(password), sizeOf(pasword));
}

Las funciones objectToAdress y sizeOf se encuentran descritas en el punto 5.4 y 5.3, respectivamente.

5.6. Herencia Múltiple.

Todos sabemos que no hay herencia múltiple en Java, salvo porque podemos hacer castings de cualquier tipo a cualquier objeto.

Para ello nos valemos de la clase Unsafe de nuevo, añadiendo (por ejemplo) la clase String a las superclases de Integer:

public void addStringClassToIntegerSuperClass(){
  long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
  long strClassAddress = normalize(getUnsafe().getInt("", 4L));
  getUnsafe().putAddress(intClassAddress + 36, strClassAddress);
}

Ahora podemos hacer castings de Integer a String sin tener una excepción en tiempo de ejecución. La única pega es que hay
que hacer una casting a la clase Object primero (para engañar al compilador):

(String) (Object) (new Integer(42))

Una manera de implementar esto sin recurrir al Unsafe sería usando Mixins. Nuestro compañero Alejandro Pérez hizo un
tutorial al respecto.

5.7. Clases Dinámicas.

Con el uso de Unsafe podemos crear clases en tiempo de ejecución. Para lograrlo, leemos los contenidos de un fichero compilado .class,
los pasamos a un array de byte y se lo pasamos al método defineClass:

Creamos una clase de ejemplo con un método que imprima un mensaje:

DinamicClass.java
package com.autentia.tutoriales.unsafe.dinamicclasscreation;

public class DinamicClass {

    public void printMessage(String message) {
        System.out.println(message);
    }
}

Ahora creamos un método que lea el contenido del fichero .class:

public byte[] getClassContent(String filePath) throws Exception {
  final File f = new File(filePath);
  final FileInputStream input = new FileInputStream(f);

  final byte[] content = new byte[(int)f.length()];
  input.read(content);
  input.close();

  return content;
}

Y probamos la ejecución (de nuevo recordad lo de gestionar de forma correcta las excepciones).

public void testDinamicClassCreation() {
  byte[] classContents;
  final String message = "Clase dinámica creada";
  try {
    classContents = getClassContent(FILE_PATH);

    final Class dinamicClass = getUnsafe().defineClass(null, classContents, 0,
            classContents.length, null, null);

    // imprime 'Clase Dinámica creada'
    dinamicClass.getMethod("printMessage", String.class).invoke(dinamicClass.newInstance(), message);
  } catch (final Exception e) {
    System.err.println(e.getMessage());
  }
}

Nota: se puede ver que en la función getMehod se ha añadido el tipo de parámetro del método a invocar,
y a la hora de invocar le pasamos el parámetro.

La forma «correcta» de crear clases dinámicamente sería la que indica Jakob Jenkov aquí.

Como conclusión de este apartado, sólo indicar que esta forma de usar Unsafe puede sernos útil a
la hora de crear Clases de forma dinámica o crear proxies o aspectos
para código existente.

5.8. Lanzar Excepciones.

Si eres de los que no les gustan las excepciones comprobadas (Checked Exceptions), ¡estás de suerte!

getUnsafe().throwException(new IOException());

El método lanza una excepción controlada, pero no se fuerza al código a contemplarla o re-lanzarla (como una excepción en tiempo de ejecución).

Si queremos realizarlo sin recurrir a la clase Unsafe, basta con crearnos excepciones que extiendan RuntimeException.

5.9. Serialización Rápida.

Todo el mundo sabe que la capacidad de Serializable para serializar (valga la redundancia) es muy lenta. Además requiere que la clase tenga un constructor
público sin argumentos. Externalizable es bastante mejor, pero se necesita un esquema para que la clase se serialice.

Hay librerías como kyro que permiten un alto rendimiento, pero tienen ciertas dependencias, cosa inaceptable si disponemos
de poca memoria. Además, en esta librería se intenta usar Unsafe: https://code.google.com/p/kryo/issues/detail?id=75

Ahora bien, gracias a nuestra «ya casi amiga» clase Unsafe podemos hacer serialización de una manera más rápida:

Para la serialización:

  • Crea un esquema para el objeto usando reflexión. Se puede hacer una única vez por clase.
  • Usa los métodos de Unsafe: getLong, getInt, getObject, etc. para tomar el valor actual de los campos.
  • Añade el identificador class para poder restaurar el objeto.
  • Listo. Escribe el objeto al fichero/salida que quieras.

También podrías añadir compresión para ahorrar espacio.

Para la des-serialización:

  • Crea una instancia de la clase serializada. Para esto puede ser útil el método allocateInstance, ya que no requiere constructor.
  • Construye el esquema, al igual que en el paso 1 de serialización.
  • Lee los campos del fichero/entrada usado.
  • Usa los métodos de Unsafe: getLong, getInt, getObject, etc. para rellenar los valores del objeto.

Aunque hay más detalles que los mostrados aquí, creo que la idea principal está bastante clara. Esta serialización será muy rápida.

5.10. Arrays grandes.

Todos sabemos que la constante Integer.MAX_VALUE es el máximo tamaño que puede tener un array en Java, ¿verdad? ¡PUES NO! Usando la
asignación directa de memoria podemos conseguir arrays tan grandes como nos permita el tamaño del ‘heap’.

Veamos una implementación de este tipo de «monstruos»:

DinamicClass.java
package com.autentia.tutoriales.unsafe.superarrays;

public class SuperArray {

  private static final int BYTE_SIZE = 1;

  private final long size;

  private final long address;

  public SuperArray(long size) throws Exception {
    this.size = size;
    address = getUnsafe().allocateMemory(size * BYTE_SIZE);
  }

  public boolean setValueAt(long index, byte value) {

    if (!checkIndex(index)) {
      return false;
    }

    try {

      getUnsafe().putByte(address + (index * BYTE_SIZE), value);

    } catch (final Exception e) {
      System.err.println(e.getMessage());
      return false;
    }

    return true;
  }

  public Integer getValueAt(long index) {

    if (!checkIndex(index)) {
      return null;
    }

    try {
      return Integer.valueOf(getUnsafe().getByte(address + (index * BYTE_SIZE)));
    } catch (final Exception e) {
      System.err.println(e.getMessage());
      return null;
    }
  }

  private boolean checkIndex(long index) {

    if (index > size || index < 0) {
      return false;
    }
    return true;
  }

  public long size() {
    return size;
  }
  public void destroy() throws Exception {
    getUnsafe().freeMemory(address);
  }
}

Y ahora, un caso de uso que muestre que efectivamente podemos poner el tamaño que queramos:

public void testSuyperArray() {
  final long HUGE_SIZE = (long)Integer.MAX_VALUE * 2;

  SuperArray superArray = null;

  try {
    superArray = new SuperArray(HUGE_SIZE);
  } catch (final Exception e) {
    System.err.println(e.getMessage());
  }

  System.out.println("Tamaño de Array: " + superArray.size());

  putValuesinArray(superArray);

  printChargedValues(superArray);
  
  try{
    superArray.destroy();
  }catch(Exception e){
    System.err.println(e.getMessage());
  }
}

private static void putValuesinArray(SuperArray superArray) {

  for (int i = 0; i < 100; i++) {
    final long pos = (long)Integer.MAX_VALUE + i;

    superArray.setValueAt(pos, (byte)3);
  }
}

private static void printChargedValues(SuperArray superArray) {
  for (int i = 0; i < 100; i++) {
    final long pos = (long)Integer.MAX_VALUE + i;
    System.out.println("Valor en posicion [" + pos + "]: " + superArray.getValueAt(pos));
  }

}

La salida mostrará el tamaño del array (4294967294) y 100 veces la línea:

Valor en posicion [*posicion*]: 3

Con esto comprobamos que efectivamente, no hay límite de tamaño en nuestro SuperArray.

NOTA: Adivinad que viene aquí… Efectivamente, ojo con el tratamiento de Excepciones y uso de parámetros
de entrada como salida.

Esta técnica está parcialmente disponible en el paquete java.nio.

Importante: La memoria asiganada de esta forma no está en el heap ni bajo la gestión del GC, así que hay que acordarse de
liberarla una vez la hemos terminado de usar. Además por defecto no se comprueba la posición donde se escribe por lo que un acceso ilegal podría
terminar colgando la JVM.

Pero entonces… ¿Para qué es esto útil? Muy sencillo: Cálculos para matemáticas y programación eficiente, donde el rendimiento es clave y se suele trabajar
con tamaños de arrays de datos muy grandes.

De todas formas, Java incorpora la clase estática BigArrays para ayudar
a tratar con ellos sin usar la clase Unsafe.

5.11. Concurrencia.

Por último (ya era hora :-P), unas pocas palabras sobre concurrencia.

Unsafe nos permite implementar estructuras de datos sin bloqueo de alto rendimiento. Esto es, nos permite usar estrucutras de datos de forma
concurrente (en otro hilo) de forma eficiente y sin bloquear el programa principal.

Por ejemplo, supongamos que necesitamos incrementar un valor en un objeto compartido usando varios hilos. Lo primero que habría que hacer sería crear la interfaz
Counter:

Counter.java
package com.autentia.tutoriales.unsafe.concurrency.counters;

public interface Counter {

    void increment();

    long getCurrentValue();
}

Acto seguido, implementamos un hilo cliente sencillo que utilice el contador:

CounterClient.java
package com.autentia.tutoriales.unsafe.concurrency;

import com.autentia.tutoriales.unsafe.concurrency.counters.Counter;

public class CounterClient implements Runnable {

  private final Counter counter;

  private final int value;

  public CounterClient(Counter counter, int value) {
    this.counter = counter;
    this.value = value;
  }

  @Override
  public void run() {
    for (int i = 0; i < value; i++) {
        counter.increment();
    }
  }

}  

Y un pequeño programa que pruebe su funcionalidad:

package com.autentia.tutoriales.unsafe.concurrency;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.autentia.tutoriales.unsafe.concurrency.counters.Counter;

public class ConcurrencyMain {

    private static final int NUM_OF_THREADS = 1000;

    private static final int NUM_OF_INCREMENTS = 100000;

    public static void main(String[] args) {


        final ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
        final Counter counter = ... // creación de contador específico

        long before = System.currentTimeMillis();

        runThreads(service, counter);

        service.shutdown();

        service.awaitTermination(1, TimeUnit.MINUTES);

        final long after = System.currentTimeMillis();

        System.out.println("Valor final del contador: " + counter.getCurrentValue());
        System.out.println("Tiempo transcurrido: " + (after - before) + " ms");
    }

    private static void runThreads(final ExecutorService service, final Counter counter) {
        for (int i = 0; i < NUM_OF_THREADS; i++) {
            service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
        }
    }

}

Ahora, al lío: Una de las mejores opciones en cuanto a rendimiento/concurrencia es el uso de ReentrantReadWriteLock. Creamos un contador que use la clase:

ReentrantReadWriteLockCounter.java
package com.autentia.tutoriales.unsafe.concurrency.counters;

import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class ReentrantReadWriteLockCounter implements Counter {

  private long value = 0;

  private final WriteLock lock = new ReentrantReadWriteLock().writeLock();

  @Override
  public void increment() {
    lock.lock();
    value++;
    lock.unlock();
  }

  @Override
  public long getCurrentValue() {
    return value;
  }
}

Y lanzamos el programa anterior con una instancia de nuestra primera implementación de contador usando esta clase. La salida obtenida es:

Valor final del contador: 100000000
Tiempo trasncurrido: 5359 ms

Ahora probamos una implementación del contador usando la clase Unsafe:

UnsafeCounter.java
package com.autentia.tutoriales.unsafe.concurrency.counters;

import sun.misc.Unsafe;

public class UnsafeCounter implements Counter {

  private volatile long currentValue = 0;

  private final Unsafe unsafe;

  private final long offset;

  public UnsafeCounter() throws Exception {
    unsafe = getUnsafe();
    offset = unsafe.objectFieldOffset(UnsafeCounter.class.getDeclaredField("currentValue"));
  }

  @Override
  public void increment() {
    long before = currentValue;
    while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
      before = currentValue;
    }
  }

  @Override
  public long getCurrentValue() {
    return currentValue;
  }
}

Ejecutamos y la salida nos da:

Valor final del contador: 100000000
Tiempo trasncurrido: 4152 ms

Observamos una cierta mejora en el tiempo de ejecución. De hecho, algunas implementaciones como
Atomic
usa Unsafe.

La función compareAndSwapLong nos permite implementar estructuras de datos libres de bloqueos. La idea subyacente es la que sigue:

  1. Dado un estado, se crea una copia.
  2. Se modifica.
  3. Se compara con el nuevo y se intercambian (se hace swap).
  4. Se repite si falla.

Por otro lado, la palabra volatile se añade al contador para evitar el riesgo de bucles infinitos.

6. Conclusiones

Hemos visto lo que se puede llegar a hacer con esta clase, y vemos que hay métodos bastante útiles para determinadas tareas. Sin embargo, no es recomendable su uso
(quién lo iba a decir, ese nombre de clase inspiraba tanta confianza… :P).

Por lo tanto, resumen de este «tocho»:
Unsafe: está guay, pero… ¡¡NO LO USES!!

Espero que estos párrafos os hayan entretenido y sido de utilidad. El código se encuentra disponible en
Github.

¡Un saludo!

Rodrigo de Blas

7. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad