Trois défis technologiques pour adapter FFmpeg aux standards TR-03

L’industrie de la radiodiffusion aujourd’hui

« Aujourd’hui, la magie du cinéma et de la télévision, c’est une histoire de sorcellerie technique » (SMPTE, 2017). Les grands radiodiffuseurs (comme Radio Canada) dépendent en grande partie d’équipements de radiodiffusion professionnels aux standards développés par la Société des Motion Pictures et Television Engineers® (SMPTE®). En plus d’arrimer par des standards universels, les diffuseurs et les fournisseurs d’équipements, la SMPTE contribue à faire progresser l’ingénierie de l’image dans l’industrie mondiale de la radiodiffusion. Présentement, l’industrie se base sur des équipements puissants mais lourds, capables de transmettre des données brutes (c’est-à-dire des signaux vidéo numériques non compressés et non cryptés) vers des équipements (terminaux) de télédiffusion (TV, ordinateur, mobile, etc.). Récemment, notre équipe d’ingénieurs s’est lancée dans l’aventure (soutenue financièrement par Radio Canada) pour tester une hypothèse technologique : Transmettre des données brutes de 3,5 Gbps, à l’aide de la plateforme FFmpeg et d’un serveur standard, le tout, sans passer par un équipement spécialisé.

Savoir-faire Linux relève le défi
L’hiver fut froid et enneigé à Montréal. Nous nous sommes donc réchauffés en passant du temps sur l’antre de FFmpeg. Notre but était de mettre en place un pipeline TR-03/SDI pouvant traiter plusieurs flux HD sur un serveur standard, tout en fabricant des dérivés basse définition du même flux grâce à FFmpeg.
Image result
Logo de FFmpeg

Plusieurs enjeux sont apparus sur notre chemin. Tout d’abord, le format TR-03 ; celui-ci n’était pas supporté dans la version upstream de FFmpeg. Le second fut la quantité de données à traiter : on parle d’un débit d’environ 3Gb/s, ce qui, d’après les développeurs de FFmpeg, n’était probablement pas possible à traiter. Enfin, il nous fallait ajouter du transcodage dans le pipeline, ce qui impliquait une charge encore plus importante sur le CPU.

L’implémentation de TR-03 ne fut pas le plus grand défi car le format vidéo était relativement simple. Nous avons donc rapidement mis en route un émetteur en utilisant GStreamer, qui possédait déjà les capacités nécessaires à l’envoi d’un tel volume de données.

Big Buck Bunny est un court métrage d’animation animé de l’Institut Blender, publié sous la forme d’un film open source sous Creative Commons License Attribution 3.0.

Une fois le flux de données en place, les freins à la performance sont devenus plus évidents. Pour identifier clairement ces freins, nous avons écrit plusieurs scénarios de référence utilisant des tests unitaires et LTTng (le roi des traceurs système sur Linux). Cela nous a permis de mettre le doigt où nous perdions des paquets, en l’occurrence, ici au niveau de la socket dans le noyau, et au niveau des buffers de l’interface réseau. Étant donné que nous contrôlions étroitement le traitement des données, il fut relativement facile d’ajuster la taille de ces buffers tout en conservant des délais acceptables. Ensuite, nous avons remarqué que le thread de FFmpeg responsable de la collecte et de décodage des données monopolisait à lui seul tout un cœur du CPU, causant occasionnellement une perte de paquets lorsque nous n’étions pas assez rapide à récupérer les données du buffer de la socket. Pour palier à ce problème, nous avons découplé le travail de collecte des données du travail de décodage. La mise en place consciencieuse de ces étapes nous a permis d’obtenir un pipeline complet ne souffrant d’aucune perte de paquets. Nous avons donc pu célébrer notre réussite avec un bon gâteau, quelque verres et un film HD que nous pouvions enfin regarder (je ne m’attendais pas à ce que Big Buck Bunny soit si drôle à regarder).

La dernière mise à jour et les nouveaux défis

Le 5 avril dernier, les contributions développées et affinées par l’équipe ont finalement été intégrées à FFmpeg, ce qui signifie que leur preuve de concept a rencontré les normes énoncées par la communauté FFmpeg et fait désormais partie de la plateforme open source.

Pour notre équipe Ingénierie de Produits, c’est une aventure trépidante et prometteuse. En dépit de tous les risques et défis, notre équipe a empiriquement démontré la possibilité d’exécuter des pipelines de traitement SDI sur un serveur standard utilisant la plateforme FFmpeg. Cette expérience est une réussite, révélant en bout de ligne l’immense potentiel de FFmpeg dans l’industrie de la radiodiffusion. Pour continuer à adpater FFmpeg aux normes de SMPTE 2110 – en constante évolution – notre équipe fait désormais face à un autre défi de taille. FFmpeg ne supportant pas la synchronisation comme décrit dans SMTPE 2110, nos experts évaluent maintenant la possibilité d’apporter les contributions nécessaires pour relever ce nouvel enjeu !

Auteurs :

  • Damien Riegel,
  • Eloi Bail, et
  • Stepan Salenikovitch.

Analyseur logique : visualiser la latence entre deux signaux numériques en temps réel avec sigrok et matplotlib

Salae Logic AnalyzerDans mon billet précédent, je décrivais comment manipuler avec Python les données extraites d’un analyseur logique Saleae afin de visualiser la latence entre deux signaux numériques et en déduire la gigue temporelle de l’un d’entre eux. En conclusion, j’émettais l’hypothèse que le SDK de Saleae pourrait nous permettre de récupérer des informations sur les signaux en temps réel et non plus par l’intermédiaire d’un fichier CSV exporté. Mais au final, c’est un logiciel libre plus générique qui nous a fourni la meilleure solution.

La solution de Saleae et son SDK

Saleae propose aux développeurs deux solutions :

  • une API (en bêta à l’heure actuelle) qui permet uniquement de contrôler leur logiciel de visualisation depuis une socket.
  • un SDK qui permet d’étendre leur logiciel et écrire son propre analyseur de protocole.

Nous aurions pu a priori nous intéresser au SDK et écrire un analyseur qui exporte à la volée la latence entre deux signaux définis, mais cette solution est dépendante du logiciel de Saleae et de son évolution, ce qui nous garantit une très faible pérennité. De plus, le logiciel de visualisation de Saleae n’a pas pour vocation d’être utilisé avec un autre analyseur logique que ceux qu’ils produisent. Nous cherchions donc une solution plus générique et permanente.

Sigrok: une suite logicielle open-source pour les analyseurs logiques

C’est lors d’une discussion entre collègues qu’Emeric Vigier m’a mit sur la piste d’un logiciel nommé Sigrok. Ayant pour objectif de fournir une solution libre et générique pour les analyseurs logiques, Sigrok est une suite logicielle permettant d’extraire les données collectées par divers types d’analyseurs et de les afficher, voire de les traiter, à l’aide de décodeurs de protocole.

Cette suite se compose de plusieurs sous-projets :

  • libsigrok : une librairie écrite en C qui standardise l’accès aux pilotes des différents analyseurs.
  • libsigrokdecode : une librairie écrite en C qui fournit une API pour le décodage de protocole. Les décodeurs sont écrits en Python (>= 3).
  • Sigrok-cli : une interface en ligne de commande pour manipuler Sigrok.
  • PulseView : une interface graphique en Qt pour manipuler Sigrok.
  • Fx2lafw : Sigrok fournit également une implémentation open source du firmware des puces Cypress FX2 qui est la puce utilisé entre autre par Saleae dans toutes les déclinaisons de ses analyseurs logiques hormis le Logic Pro 16. Ce firmware permet de programmer le FPGA pour qu’il fonctionne comme un simple analyseur logique matériel.

Sigrok : exemple d’utilisation

Un exemple de cette suite mise bout à bout sera plus parlant qu’un long discours. Nous allons donc utiliser PulseView pour capturer et visualiser les signaux d’un analyseur logique.

À l’ouverture PulseView dans sa version 0.2.0 ressemble à ceci :
[cliquez sur les images pour les agrandir]

pulseview01

Nous allons commencer une capture en laissant le périphérique de démonstration générer des signaux aléatoires :

pulseview02

Nous allons ensuite ajouter un décodeur assez simple qui calcule le rapport cyclique d’un signal numérique :

pulseview03

Chaque décodeur dispose de ses options. Dans notre cas nous allons simplement définir le canal sur lequel nous voulons appliquer le décodeur :

pulseview04

Le calcul du rapport cyclique est appliqué sur le canal et reporté directement dans l’interface de PulseView :

pulseview05

Sigrok : écriture d’un décodeur pour le calcul de la latence entre deux signaux numériques

Ce qui suit est basé sur la libsigrok et la libsigrokdecode dans leurs version 0.3.0.
Chaque décodeur de protocole est un module Python qui possède son propre sous-répertoire dans le répertoire de décodeurs libsigrokdecode.

Un décodeur se compose de deux fichiers :

  • __init__.py : Ce fichier est requis pour l’initialisation du décodeur et contient une simple description du protocole.
  • pd.py : Ce fichier contient des métadonnées sur le décodeur, et son code, la plupart du temps implémenté dans la méthode decode()

Nous allons nous intéresser au fichier pd.py contenant le code de notre décodeur. L’API de la libsigrokdecode nous fournit toutes les informations sur les signaux capturés.

Rappelons nous que pour calculer la latence entre deux signaux numériques, notre logique est de considérer l’un des signaux comme une horloge de référence, et l’autre comme une résultante de ce premier. Les deux étant liés, à chaque transition d’état du signal d’horloge, le signal résultant changera aussi d’état avec une latence plus ou moins variable. Nous parlerons de la fluctuation de cette latence plus loin.

Les options du décodeur

La première chose à faire est donc de permettre à notre décodeur de définir quel canal correspond au signal d’horloge et quel canal correspond au signal résultant. La classe du décodeur permet justement de définir quelques attributs précisant ses options, notamment celle permettant à l’utilisateur de choisir les canaux :

class Decoder(srd.Decoder):
   # ...
   channels = (
      {'id': 'clk', 'name': 'Clock', 'desc': 'Clock reference channel'},
      {'id': 'sig', 'name': 'Resulting signal', 'desc': 'Resulting signal controlled by the clock'},
   )
   ...

La méthode decode()

La seconde étape va être d’implémenter le contenu de la méthode decode(). Cette méthode est appelé par la libsigrokdecode chaque fois qu’un nouveau bloc de données à traiter est disponible.

Chaque bloc est en fait un échantillon dépendant de la fréquence d’échantillonnage de nos signaux. Par exemple pour une valeur d’échantillonnage de 100 Hz, nous obtiendrons 100 blocs de données par seconde. Ces blocs de données contiennent le numéro de l’échantillon actuel, ainsi que l’état des différents signaux à cet instant.

À partir de ces informations, il est ainsi très facile d’implémenter une machine à état qui va noter à quel échantillon à eu lieu la transition du signal d’horloge et celle du signal résultant. Le nombre d’échantillons écoulés entre les deux, multiplié par la valeur de la fréquence d’échantillonnage, va nous donner la latence exprimée en secondes.

Dans sa version simplifiée, notre machine à état ressemble à cela :

def decode(self, ss, es, data):

    self.oldpin, (clk, sig) = pins, pins

    # State machine:
    # For each sample we can move 2 steps forward in the state machine.
    while True:
        # Clock state has the lead.
        if self.state == 'CLK':
            if self.clk_start == self.samplenum:
                # Clock transition already treated.
                # We have done everything we can with this sample.
                break
            else:
                if self.clk_edge(self.oldclk, clk) is True:
                    # Clock edge found.
                    # We note the sample and move to the next state.
                    self.clk_start = self.samplenum
                    self.state = 'SIG'

        if self.state == 'SIG':
            if self.sig_start == self.samplenum:
                # Signal transition already treated.
                # We have done everything we can with this sample.
                break
            else:
                if self.sig_edge(self.oldsig, sig) is True:
                    # Signal edge found.
                    # We note the sample, calculate the latency
                    # and move to the next state.
                    self.sig_start = self.samplenum
                    self.state = 'CLK'
                    # Calculate and report the latency.
                    self.putx((self.sig_start - self.clk_start) / self.samplerate)

    # Save current CLK/SIG values for the next round.
    self.oldclk, self.oldsig = clk, sig

Les sorties de notre décodeur

La libsigrokdecode propose différentes façons de remonter les informations de notre décodeur. Une méthode register() permet d’enregistrer les sorties que notre décodeur génère. Chaque sortie a un type défini; par exemple, le type OUTPUT_ANN est utilisé pour définir une sortie de type annotation qui sera représentée dans PulseView par les boîtes graphiques que nous avons vues précédemment avec le décodeur de rapport cyclique.

Pour notre décodeur, nous voulons principalement deux types de sorties:

  • une sortie de type annotation (OUTPUT_ANN) pour visualiser la latence mise en forme dans PulseView,
  • et une sortie de type binaire (OUTPUT_BINARY) pour sortir les valeurs de latence brute sans mise en forme afin d’être analysé.

La version finale de notre décodeur inclut également une sortie de type métadonnée (OUTPUT_META) rapportant des statistiques sur les signaux manqués, mais ce n’est pas important ici.

Nos deux sorties donc sont définies dans notre décodeur comme suit :

self.out_ann = self.register(srd.OUTPUT_ANN)
self.out_bin = self.register(srd.OUTPUT_BINARY)

Pour reporter des informations sur ces sorties nous utilisons la méthode put() qui a pour prototype :

put(debut_echantillon, fin_echantillon, type_de_sortie, donnees)

type_de_sortie est une de nos deux méthodes enregistrées au préalable (out_ann, out_bin).

Pour reporter la latence entre deux signaux en utilisant le type annotation nous pourrons par exemple écrire :

put(clk_start, sig_start, out_ann, [0, [ma_latence]])

Les résultats obtenus avec notre décodeur de latence

Voici un exemple des résultats obtenus avec notre décodeur dans PulseView :
pulseview06

Et les résultats obtenus avec sigrok-cli dans sa version 0.5.0 et la sortie binaire pour avoir les valeurs de latence brute :
sigrok-cli

Visualisation des données en temps réel pour en déduire la gigue temporelle

Comme nous venons de le voir, sigrok-cli nous permet de sortir les valeurs de latence brute sur la console en temps réel. Nous allons maintenant nous intéresser aux variations de cette latence. En effet, rien ne nous garantit que la latence entre ces deux signaux sera constante.

Considérant que le signal d’horloge est périodique et sans fluctuation, en théorie, le signal résultant devrait l’être aussi et la latence entre ces deux signaux devraient être constante. Si ce n’est pas le cas, c’est qu’il y a une fluctuation de cette latence que nous avions décrite dans le précédent article comme la gigue temporelle d’un signal, et c’est ce que nous voulons visualiser maintenant.

Nous pouvons donc imaginer récupérer ces valeurs à la volée et les mettre en forme dans un graphique. Pour cela, nous allons encore une fois écrire un script en Python.

Je me suis intéressé à la librairie matplotlib et son module d’animation, qui propose une méthode FuncAnimation() permettant de définir une fonction à appeler pour mettre à jour le graphique à chaque fois que de nouvelles données sont disponibles. Cette fonction prend en paramètre la figure sur laquelle nous travaillons, la fonction d’animation à appeler et l’ensemble de données à traiter.

anim = animation.FuncAnimation(fig, animate, frames=data_gen)

Ces données peuvent être de la forme d’un générateur Python qui va très bien de paire avec la lecture d’un flux de données (merci à Guillaume Roguez pour m’avoir présenté ce type d’objet).

Ainsi à chaque fois qu’une nouvelle latence sera écrite dans le flux, notre générateur récupérera une nouvelle donnée et la fonction d’animation sera appelée.

Voici à quoi ressemble le code du générateur :

# The data generator take its
# input from file or stdin
def data_gen():
    while True:
        line = fd.readline().strip()
        if line:
            yield line

Notre fonction d’animation va, si besoin est, mettre à jour l’abscisse pour visualiser toutes les latences, et ajouter la nouvelle valeur fournie par le générateur.

# The update graph function
def animate(data):
    global x_max, x_min, x, y

    try:
        # we must recalculate the abscissa range
        x_new = float(data)
        if x_max is None or x_new > x_max:
            x_max = x_new
        if x_min is None or x_new < x_min:
            x_min = x_new
        ax.set_xlim(x_min, x_max)

        # add the new plot coordinate
        x.append(x_new)
        y.append(0)
        line.set_data(x, y)

        return line,

    except KeyboardInterrupt:
        print("leaving...")
&#91;/code&#93;

Finalement, il nous reste plus qu'à éxécuter <code>sigrok-cli</code> avec notre décodeur de latence et à récupérer ces valeurs dans notre script de visualisation.


sigrok-cli -d fx2lafw --config samplerate=24MHz --samples 2M -C 1,2 -P jitter:clk=1:sig=2 -B jitter | rt-draw.py

Ce qui nous donne le résultat final suivant et qui nous permet de visualiser la gigue temporelle du signal résultant :

rt_draw

Travailler les données d’un analyseur logique Saleae en Python

Salae Logic AnalyzerRécemment, j’ai été amené à devoir prendre des mesures de temps très précises pour répondre à une problématique de contrainte temps-réel. Sur un SoC particulier avec un système Linux dessus, la question était de savoir quelle était la fluctuation de la latence entre le déclenchement d’un événement matériel et la réponse d’une tâche logicielle associée à cet événement.

La gigue ou fluctuation du signal

Qu’est ce que la gigue ou jitter en anglais ? Prenons, par exemple, un événement avec une fréquence parfaite — disons l’heure à laquelle sonne mon réveil — et un autre événement moins parfait qui lui est associé, comme le moment où j’ouvre les yeux. Chaque jour, l’heure de mon réveil reste la même. En revanche, le moment où j’ouvre les yeux pour éteindre mon réveil varie… au désespoir de mon patron. :/

Cette fluctuation est la gigue. Dans ce cas, il s’agit d’une gigue temporelle, comme dans ma problématique.

Mesurer la gigue avec un oscilloscope

Typiquement, mesurer la latence entre deux événements matériels se fait très bien avec un oscilloscope à deux canaux. Il suffit de savoir où sonder, sur notre circuit, ces événements. Après quelques réglages, on peut visualiser la latence et, notamment, avoir une persistance sur chaque prise de mesure qui va nous donner une enveloppe de cette fluctuation.

Ma première démarche à donc été d’associer à ma tâche logicielle un autre événement matériel pour m’aider à prendre ces mesures. Mon événement matériel initial était une interruption et je savais où la sonder sur le SoC. À celui-ci, je réponds par un autre événement matériel qui consiste à faire commuter une GPIO que je sais également où sonder sur le SoC.

Ainsi à chaque fois que l’interruption se déclenche, la GPIO change d’état.

Ce qui nous donnera par exemple sur un oscilloscope quelque chose comme ça :

jitter_simulated

Avec en jaune l’interruption qui est réguliére, et en vert la fluctuation des temps de réponse de la GPIO.

Il ne me restait plus qu’à prendre mes mesures avec un oscilloscope. Mais là c’est le drame… je ne disposais pas d’oscilloscope suffisamment récent pour avoir une enveloppe consistante de mes signaux; faute de mémoire, je pouvais conserver tout au plus les 32 dernières mesures et je ne pouvais donc pas voir ma fluctuation sur de longues périodes.

Le seul autre équipement à ma disposition pour prendre des mesures était un analyseur logique Saleae Logic. Bien que très pratique, ce dernier ne me permettait pas de visualiser ma fluctuation du signal.

Comment visualiser la gigue avec l’analyseur logique de Saleae ?

Avec cet analyseur, ce que je peux obtenir comme résultat ressemble à cela:

logic_output

En zoomant on voit bien la latence entre le déclenchement de l’interruption (en haut) et la commutation de ma GPIO (en bas):

logic_output_zoom

Ce qui m’intéresse, c’est mesurer étant le temps entre le drapeau 1 et le drapeau 2 pour chaque interruption et le visualiser de façon facilement compréhensible, comme ceci:

logic_output_zoom_flag

Mais je ne peux pas faire ça à la main pour chaque interruption et recommencer à chaque prise de mesure.

C’est en discutant de mon problème, qu’un collègue et ami (Emeric Vigier, pour ne pas le nommer), me pointa à juste titre sur la fonction d’exportation de ce logiciel. Et, effectivement, l’ensemble des informations qui me sont nécessaires sont à ma disposition, le dernier problème étant juste qu’elles ne sont pas organisées comme je le désire.

Visualisation des données : du CSV au Graphique

Donc, comme précisé précédemment, l’analyseur logique de Saleae me permet d’exporter mes données au format CSV sous la forme:

Time[s], Interrupt Trigger, Gpio 0
0, 0, 1
0.00258295833333333, 1, 1
0.00258408333333333, 0, 1
0.002633, 0, 0
0.00758641666666667, 1, 0
0.00758758333333333, 0, 0
0.007629625, 0, 1
0.0125899166666667, 1, 1
...

La première valeur me donnant le temps en seconde, la deuxième l’état de mon interruption (0 = bas, 1 = haut), et la troisième l’état de ma GPIO. Donc, l’algorithme me permettant de récupérer ma latence sera:

Pour chaque ligne,

  1. Si mon interruption passe de 0 à 1, Alors je note son temps dans A
  2. Si j’ai A et que ma GPIO change d’état, Alors je note son temps dans B
  3. Si j’ai A et B, Alors je soustrais A à B et je garde le résultat dans le tableau C

Le tableau C contiendra ainsi l’ensemble des temps de latence. En quelques lignes de Python, cela donne le code suivant :

STEP_INTERRUPT  = 0
STEP_GPIO       = 1

with open(file_csv, 'r') as csvfile:
    cvsreader = csv.reader(csvfile, delimiter=',')

    # the first entry give us the initial state
    init_state      = next(cvsreader)
    interrupt_state = init_state[interrupt]
    gpio_state      = init_state[gpio]
    step            = STEP_INTERRUPT

    for row in cvsreader:

        # The first step is to find an interrupt edge
        if step == STEP_INTERRUPT:
            if (interrupt_state == 0 and int(row[interrupt]) == 1):
                interrupt_value = float(row[0])
                step = STEP_GPIO

        # The second step is to find a gpio commutation
        elif step == STEP_GPIO:
            if (gpio_state != int(row[gpio])):
                gpio_value = float(row[0])
               
                # here we know we have an interrupt edge value
                # and the gpio commutation time
                # we can take the delay mesure between the two
                delay = gpio_value - interrupt_value

                # store the delay value in millisecond.
                dic.append((delay * 1000, 0.5))

                # we can now reinit the step
                step = STEP_INTERRUPT

        interrupt_state = int(row[interrupt])
        gpio_state      = int(row[gpio])

À ce stade, je dispose de l’ensemble de mes latences, maintenant j’aimerais bien pouvoir les mettre en forme avec quelque chose de plus parlant — et ce n’est pas comme si l’on manquait de librairies pour réaliser des graphiques dans Python. Donc, prenons en une au hasard, et dessinons!

J’ai choisi la librairie pygal, qui permet de dessiner des points dans un espace à 2 dimensions. Il m’a suffit de rajouter ces quatre lignes de Python à mon script:

xy_chart = pygal.XY(stroke=False, fill=False, show_y_labels=False, legend_at_bottom=True, show_dots=True, dots_size=0.8, print_values=False)
xy_chart.title = 'Jiffies'
xy_chart.add('values in milli-second', sorted(dic))
xy_chart.render_to_png('jitter.png')

Et voilà le résultat tant désiré car nettement plus parlant:

jitter-with-pygal

On peut ainsi voir quelque chose qui se rapproche de notre première capture d’écran (l’exemple d’un oscilloscope) tout en économisant quelques milliers de dollars… cette fois, au grand bonheur de mon patron. 😉

Le script complet est disponible ici : github.com/sbourdelin/logic-jitter

Les demandes de permission inexpliquées des applications Android doivent-elles nous inquiéter?

Capture d'écran (Scrabble)J’ai plus de 120 applications installées sur mon téléphone intelligent. Oui, je sais, c’est beaucoup, d’autant que certaines disposent d’une foule de permissions. Régulièrement, mon système d’exploitation mobile m’informe que l’une d’entre elles n’en a pas encore assez. Il lui en faut plus, mais l’éditeur ne dit pas pourquoi. À première vue, cela me semble dangereux, mais qu’en est-il vraiment?

Pour le savoir, j’ai discuté cette semaine avec Émeric Vigier et Alexandre Lision, deux ingénieurs logiciels de Savoir-faire Linux impliqués dans plusieurs projets d’applications mobiles, dont SFLphone pour Android, actuellement en phase bêta.


Christian – Récemment, j’ai envoyé un courriel au développeur d’un jeu de Scrabble dont la mise à jour réclamait cette nouvelle permission : « Accès aux images, vidéos et fichiers audio stockés sur l’appareil, ainsi que sur un support de stockage externe. » Je voulais savoir pourquoi. Il m’a répondu que « le systeme Android ne permet pas un contrôle très fin des permissions. La seule action potentielle visée par cette permission consiste à enregistrer (par un appui long sur le bouton « Mélanger ») une capture d’écran qui peut m’être envoyée par mail à des fins de débogage. » Puis-je le croire sur parole? Et pourquoi ne pas l’expliquer clairement dès le départ?

Émeric VigierÉmeric Vigier – C’est une réponse sensée. Aujourd’hui, la plupart des applications mobiles demandent également l’accès aux photos et au système de fichiers afin de pouvoir changer un avatar, créer un fond d’écran, etc. S’il s’agit d’une application en laquelle tu avais confiance auparavant et qui a bien fonctionné jusqu’ici, la nouvelle autorisation correspond probablement à l’ajout d’une fonctionnalité.

Aurais-tu des exemples d’applications qui t’ont trahi ou qui ont vendu des photos de nus présentes sur ton téléphone? (rires) Les applications d’ordinateur n’ont pas besoin d’autorisation pour accéder à ces données et personne ne trouve cela inacceptable.

Alexandre LisionAlexandre LisionJe suis d’accord avec Émeric. Les nouvelles autorisations ne sont pas forcément injustifiées et correspondent souvent à l’ajout de fonctionnalités. Le NFC ou la nouvelle génération de composants Bluetooth Low Energy, par exemple, offrent de nouvelles possibilités qui se traduisent par de nouvelles fonctionnalités et, donc, de nouvelles permissions. Google a le mérite de lister clairement à l’utilisateur la liste des autorisations avant le téléchargement. Et puis les permissions ont été grandement affinées, regarde la liste.

Alors, certes, il est toujours possible qu’une permission soit détournée de son usage premier, mais si tu pars de ce principe, tu ne fais plus grand chose avec ton téléphone. Les permissions pourraient être encore plus affinées (par exemple ne donner accès qu’à un dossier de photo spécifique au lieu de tous) mais cela alourdirait considérablement la configuration d’une application. Et la liste de permissions ressemblerait aux conditions d’utilisations d’iTunes, que personne ne lit ou à peu près.

ChristianD’accord. Reste qu’une saine transparence de la part des développeurs pourrait s’inscrire parmi les bonnes pratiques de l’industrie, non? Si les développeurs prenaient la peine d’informer le public des objectifs précis visés par chaque permission, ils informeraient et éduquerait le public qui, actuellement, ne s’en soucie guère. Cette divulgation constituerait aussi un engagement explicite de leur part de ne pas dépasser un périmètre d’action donné. Cela permettrait à des geeks, des médias ou des associations de vérifier qu’ils sont effectivement de bonne foi. En cas d’activité non documentée, le public finirait par le savoir. Bref, cette communication ouverte permettrait de maintenir un lien bidirectionnel de confiance et d’imputabilité entre les deux parties.

Émeric – C’est un vœu pieu, mais à mon avis idéaliste. Rien n’empêcherait un hacker de programmer une application réalisant une fonction de retouche d’image et documentant le fait qu’il a besoin d’accéder à tes images pour le faire. Tu l’accepterais et, en arrière-plan, l’application ferait de la reconnaissance d’image sur tes photos pour potentiellement reconnaître (feu) Oussama Ben Laden et transmettre l’info au gouvernement américain. Aucune divulgation volontaire ne pourrait empêcher les applications frauduleuses.

AlexandreIl faut quand même noter que Facebook s’est lancé dans l’explication de chacune des permissions que son application Android demande. Je n’ai pas d’autre exemple en tête, mais je sais que certains développeurs Android donnent la raison des permissions requises par leur application sur le Play Store. Au final, l’utilisateur devient quand même responsable à partir du moment où il clique sur le bouton « Accepter ». C’est à lui d’évaluer les risques qu’il est prêt a prendre.

En cas d’abus, un utilisateur geek aura évidemment plus de chance de tirer la sonnette d’alarme et de propager l’info. De nombreux outils permettent de vérifier le fonctionnement d’une application – wireshark pour la capture du traffic réseau, android debug bridge pour afficher la console d’événement du téléphone dans un terminal, etc. Les moins geeks auront intérêt à n’installer que des applications bien notées dans le Play Store ou provenant d’une source sûre. Il est en revanche beaucoup plus dangereux d’installer une application téléchargée sur un site inconnu et recommandée par personne, car il est peu probable qu’elle soit passée par un quelconque processus de validation.

ChristianÀ cet égard, est-ce que les utilisateurs de iPhone sont mieux protégés que ceux d’Android?

Émeric – Les dernières versions d’Android sont beaucoup mieux protégés que les précédentes. Le problème, c’est que très peu d’utilisateurs ont la dernière version, car le travail de mise à jour est dévolu au manufacturier de l’appareil et que celui-ci ne fournit que rarement cet effort lorsqu’il s’agit de téléphones bon marché.

Avec iOS, c’est différent. Apple fournit le matériel ET le logiciel. Ils ont infiniment moins de modèles différents à maintenir. C’est ainsi que Tim Cook peut se vanter que 90 % des utilisateurs d’iPhone ont la dernière version. Et c’est en ce sens qu’ils sont mieux protégés que les possesseurs d’Android.

D’un autre côté, le modèle « Google et Apple contrôlent tout » n’est pas mieux. Il faut bien comprendre que dans tous les cas, même si tu n’installes aucune application, tes données personnelles seront quand même accessibles par le constructeur (Apple, Samsung, etc.), donc par un certain nombre d’employés, de partenaires ou d’organisations. [NDLR: voir à ce sujet les conjectures récentes de Jonathan Zdziarski sur iOs]

Le dernier paragraphe de cet article propose que les utilisateurs d’Android puissent activer ou désactiver chacune des permissions de leurs applications. Cela donnerait pour sûr du travail aux développeurs qui devraient s’assurer que la leur fonctionne dans de nombreuses configurations différentes. Mais cela pourrait répondre efficacement à tes craintes.

AlexandreIl faut aussi tenir compte des réactions humaines. Avec Apple, tu ne découvres pas les permissions requises lors de l’installation de l’application, mais pendant son utilisation. Tu peux alors en refuser certaines, mais le fait est que l’utilisateur est mis devant le fait accompli, ce qui peut avoir un effet plus pernicieux. Au lieu de s’interroger sur la logique de l’application ou sur la crédibilité du développeur, on va souvent se dire&nbsp: « Bah! À quoi bon m’inquiéter maintenant alors que je ne suis plus qu’à un clic de ce que je veux faire! ».

Mais, comme l’indique Émeric, Apple a démontré que l’on peut utiliser une application de façon partielle, tant et aussi longtemps que les permissions ne sont pas interdépendantes. Android adopte d’ailleurs également ce système sur certains points sensibles, comme la géolocalisation ou l’activation du Bluetooth, par exemple.

Émeric – Blackberry OS 10 (BB10) a un fonctionnement similaire : l’utilisateur peut activer et désactiver les autorisations de chaque application de manière précise. Evidemment, s’il désactive toutes les autorisations requises, il y a peu de chance que l’application fonctionne correctement. Quant à Firefox OS, je n’ai pas encore eu l’occasion de le manipuler mais j’ai l’impression, d’après ce que je vois ici, qu’il a un fonctionnement plus proche d’Android sur ce
point.

  • Dans la société hyperconnectée d’aujourd’hui, sans voir le mal partout ni renoncer au téléphone intelligent, il est préférable d’être conscient des risques qui l’accompagnent et de les maîtriser.
  • Ainsi, tout effort visant à mieux informer le public, à l’éduquer aux aspects techniques de la mobilité numérique et à lui donner plus de contrôle sur les options de confidentialité représente, à mon avis, un pas dans la bonne direction.
  • Même si cela complique les choses en fragmentant l’écosystème, le fait d’avoir le choix entre plusieurs plate-formes permet de choisir en connaissance de cause celle dont l’approche nous convient le mieux.

Comment reprogrammer une EEPROM sous Linux (2)

eepromDans un premier billet, nous avons vu qu’il était très simple de reprogrammer une mémoire de type EEPROM sous linux. Malheureusement, nous avions rencontré des soucis d’accès au périphérique que nous devons maintenant contourner. Dans ce second billet, je vous propose donc d’étudier les outils des débogage I2C Tools et leur utilisation sur un système embarqué.

Permission denied

« Permission denied » est probablement l’erreur la plus courante sous Linux. C’est aussi la moins déconcertante: si make me a sandwich ne fonctionne pas, sudo make me a sandwich le devrait. En embarqué, la commande sudo n’a pas de sens puisque nous sommes déjà root. Alors, si ce n’est pas un problème d’utilisateur, pourquoi le système ne nous laisse-t-il pas écrire dans l’EEPROM?

Use the source, Luke!

Allons jeter un œil au code source du pilote. Celui-ci dispose bien des routines pour lire et écrire dans l’EEPROM, mais il y a un élément plus intéressant : l’option AT24_FLAG_READONLY. Le pilote permet en effet aux développeurs de supprimer l’accès en écriture. En définissant cette option, le développeur empêche les applications d’utilisateur de modifier le contenu de la mémoire. L’option peut être déclarée dans des platform_data ou propriété « read-only » pour les systèmes supportant l’OpenFirmware (CONFIG_OF).

Pour faire court, sous Linux, nous pouvons classer les périphériques en deux types : les périphériques découvrables par le matériel (souris USB, carte PCI, …) et ceux qui doivent être « figés dans le code » au niveau du kernel (I2C, SPI, …). Notre mémoire EEPROM I2C tombe dans la deuxième catégorie.

I2C Tools

Si le pilote ne nous permet pas d’écrire, nous avons néanmoins une alternative : envoyer des commandes d’écriture directement sur le bus I2C. C’est la raison d’être de la chaîne d’outils I2C Tools, qui comprend quatre binaires:

  • i2cset envoie des commandes d’écriture à un périphérique,
  • i2cget envoie des commandes de lecture,
  • i2cprobe détecte les périphériques présents sur le bus,
  • et i2cdump lit l’intégralité des registres d’un périphérique.

Problème : je n’ai pas accès au système de compilation de ce système et l’ingénieur matériel qui m’accompagne n’en a pas connaissance non plus. Réfléchissons : sur quel système travaillons-nous?

# cat /proc/cpuinfo
Processor : ARM926EJ-S rev 5 (v5l)

Un ARM9, formidable! Je travaille présentement sur un autre projet équipé de ce CPU. Je devrais pouvoir utiliser la version d’i2c-tools compilée pour ARM de mon projet sur cet équipement. La version de Linux n’est pas la même (2.6.30 contre 3.2.27), mais l’API i2c.h n’a probablement pas bougé entre les deux. Essayons :

# i2cget -y 2 0x54
Error: Could not set address to 0x54: Device or resource busy

Le binaire fonctionne, l’API n’a pas dû bouger.

Opération à coeur ouvert

Évidemment le pilote de l’EEPROM a le contrôle sur ce périphérique et Linux ne nous permet pas de le hacker. Les outils I2C peuvent forcer la porte avec l’option -f, mais il y a un risque de contention du bus. Par exemple, si deux accès concurrents s’entrelacent, ils risquent de corrompre notre périphérique I2C. Nous allons plutôt utiliser la méthode élégante : déconnecter le lien entre le pilote et le périphérique. Cette méthode est disponible depuis linux-2.6.13 et est décrite sur LWN. L’idée est de supprimer le lien qui existe entre l’EEPROM et son pilote : at24. Même de cette façon, il y a un risque de corruption comme le décrit cet échange de la liste de discussion du kernel. Dans notre cas, au pire, une application accédant à l’EEPROM va planter. Jetons un œil à ce lien entre notre EEPROM et son pilote :

# cd /sys/devices/platform/i2c_davinci.2/i2c-2/2-0054/driver
# ls -l
lrwxrwxrwx 1 root root 0 Feb 1 03:43 0-0050 -> ../../../../devices/platform/i2c_davinci.0/i2c-0/0-0050
lrwxrwxrwx 1 root root 0 Feb 1 03:43 2-0054 -> ../../../../devices/platform/i2c_davinci.2/i2c-2/2-0054
--w------- 1 root root 4096 Feb 1 03:43 bind
--w------- 1 root root 4096 Feb 1 03:43 uevent
--w------- 1 root root 4096 Feb 1 03:43 unbind

Ici, on peut voir que le pilote est associé à deux mémoires EEPROM différentes : L’une à l’adresse 0x50 sur le bus i2c-0. L’autre à l’adresse 0x54 sur le bus i2c-2. C’est la deuxième qui nous intéresse. Essayons de retirer le pilote :

# echo 2-0054 > unbind

# i2cdump -y 2 0x54
No size specified (using byte-data access)
0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef
00: 4b 5a 20 20 20 20 20 20 20 20 20 20 20 20 20 20 KZ
10: 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20: 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
30: 20 20 20 20 20 20 20 20 20 20 20 20 20 20 4e 50 NP
40: 45 4e 30 31 20 20 20 20 20 20 20 20 20 20 20 20 EN01

Bingo! Essayons maintenant d’écrire un octet à l’adresse 0x44, par exemple :

# i2cset -y 2 0x54 0x44 0x5a

# i2cget -y 2 0x54 0x44
0x5a

Bingo! Quoi de plus simple? Désormais nous pouvons écrire un petit script shell ou programme C pour réécrire les octets qui nous intéressent spécifiquement et l’exécuter sur chaque équipement présentant le défaut initial. N’oublions pas de recoudre le patient, c’est à dire de reconnecter le pilote :

# echo 2-0054 > bind
[ 643.972940] at24 2-0054: 512 byte 24c04 EEPROM, writable, 1 bytes/write

Et vérifions que le patient est bien guéri en lisant l’octet modifié :

# hexdump -C 2-0054/eeprom -n 1 -s 0x44
00000044 5a |Z|

Mission réussie grâce à un petit « hack » rapide et efficace. Inutile de brancher un JTAG ou de recompiler quoi que ce soit. Reste à convaincre le chef de projet que la solution logicielle vaut le coup, ce qui risque d’être une autre histoire…