web-dev-qa-db-fra.com

Est-ce une bonne idée d'appeler des commandes Shell depuis C?

Il y a une commande Unix Shell (udevadm info -q path -n /dev/ttyUSB2) Que je veux appeler à partir d'un programme C. Avec probablement environ une semaine de lutte, je pourrais le réimplémenter moi-même, mais je ne veux pas le faire.

Est-ce une bonne pratique largement acceptée pour moi d'appeler simplement popen("my_command", "r");, ou cela introduira-t-il des problèmes de sécurité inacceptables et transmettra-t-il des problèmes de compatibilité? Cela me fait du mal de faire quelque chose comme ça, mais je ne peux pas mettre le doigt sur pourquoi ce serait mauvais.

50
johnny_boy

Ce n'est pas particulièrement mauvais, mais il y a quelques mises en garde.

  1. dans quelle mesure votre solution sera-t-elle portable? Votre binaire choisi fonctionnera-t-il de la même manière partout, produira-t-il les résultats dans le même format, etc.? Sera-t-il affiché différemment sur les paramètres de LANG etc.?
  2. combien de charge supplémentaire cela ajoute-t-il à votre processus? Forker un binaire entraîne une charge beaucoup plus importante et nécessite plus de ressources que l'exécution d'appels de bibliothèque (en général). Est-ce acceptable dans votre scénario?
  3. Y a-t-il des problèmes de sécurité? Quelqu'un peut-il remplacer le binaire que vous avez choisi par un autre et effectuer des actes néfastes par la suite? Utilisez-vous des arguments fournis par l'utilisateur pour votre binaire et pourraient-ils fournir ;rm -rf / (par exemple) (notez que certaines API vous permettront de spécifier des arguments de manière plus sécurisée que de simplement les fournir sur la ligne de commande)

Je suis généralement heureux d'exécuter des binaires lorsque je suis dans un environnement connu que je peux prédire, lorsque la sortie binaire est facile à analyser (si nécessaire - vous pouvez simplement avoir besoin d'un code de sortie) et je n'ai pas besoin de le faire aussi souvent.

Comme vous l'avez noté, l'autre problème est la quantité de travail nécessaire pour reproduire ce que fait le binaire? Utilise-t-il une bibliothèque que vous pouvez également exploiter?

59
Brian Agnew

Il est extrêmement prudent de se prémunir contre les vulnérabilités d'injection une fois que vous avez introduit un vecteur potentiel. C'est à l'avant-plan de votre esprit maintenant, mais plus tard, vous devrez peut-être sélectionner ttyUSB0-3, alors cette liste sera utilisée dans d'autres endroits afin qu'elle soit prise en compte pour suivre le principe de la responsabilité unique, puis un client devra mettre un appareil arbitraire dans la liste, et le développeur faisant que le changement n'aura aucune idée que la liste finira par être utilisée de manière dangereuse.

En d'autres termes, codez comme si le développeur le plus imprudent que vous connaissez effectuait un changement dangereux dans une partie apparemment sans rapport du code.

Deuxièmement, la sortie des outils CLI n'est généralement pas considérée comme une interface stable, sauf si la documentation les marque spécifiquement comme telles. Vous pouvez peut-être compter sur eux pour un script que vous exécutez que vous pouvez résoudre vous-même, mais pas pour quelque chose que vous déployez chez un client.

Troisièmement, si vous voulez extraire facilement une valeur de votre logiciel, il y a de fortes chances que quelqu'un d'autre le veuille aussi. Recherchez une bibliothèque qui fait déjà ce que vous voulez. libudev était déjà installé sur mon système:

#include <libudev.h>
#include <sys/stat.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
    struct stat statbuf;

    if (stat("dev/ttyUSB2", &statbuf) < 0)
        return -1;
    struct udev* udev = udev_new();
    struct udev_device *dev = udev_device_new_from_devnum(udev, 'c', statbuf.st_rdev);

    printf("%s\n", udev_device_get_devpath(dev));

    udev_device_unref(dev);
    udev_unref(udev);
    return 0;
}

Il existe d'autres fonctionnalités utiles dans cette bibliothèque. Je suppose que si vous en avez besoin, certaines des fonctions associées peuvent également être utiles.

37
Karl Bielefeldt

Dans votre cas spécifique, où vous souhaitez invoquer udevadm, je soupçonne que vous pouvez extraire udev en tant que bibliothèque et effectuer les appels de fonction appropriés comme alternative?

par exemple, vous pouvez jeter un œil à ce que fait udevadm lui-même lorsqu'il est invoqué en mode "info": https://github.com/gentoo/eudev/blob/master/src/udev/udevadm-info.c et faire des appels équivalents à ceux que fait udevadm.

Cela éviterait beaucoup des compromis négatifs énumérés dans excellente réponse de Brian Agnew - par exemple, ne pas se fier au binaire existant à un certain chemin, éviter les frais de bifurcation, etc.

16
Adam Krouskop

Votre question semblait appeler une réponse sur la forêt, et les réponses ici ressemblent à des réponses d'arbre, alors j'ai pensé vous donner une réponse sur la forêt.

C'est très rarement comment les programmes C sont écrits. C'est toujours la façon dont les scripts Shell sont écrits, et parfois comment les programmes Python, Perl ou Ruby sont écrits.

Les gens écrivent généralement en C pour une utilisation facile des bibliothèques système et un accès direct de bas niveau aux appels système du système d'exploitation ainsi que pour la vitesse. Et C est un langage difficile à écrire, donc si les gens n'ont pas besoin de ces choses, ils n'utilisent pas C. De plus, les programmes C ne devraient généralement avoir que des dépendances sur les bibliothèques partagées et les fichiers de configuration.

Décortiquer un sous-processus n'est pas particulièrement rapide, et il ne nécessite pas un accès fin et contrôlé aux installations système de bas niveau, et il introduit une dépendance peut-être surprenante d'un exécutable externe, il est donc rare de voir dans les programmes C.

Il y a d'autres préoccupations. Les problèmes de sécurité et de portabilité mentionnés par les gens sont tout à fait valables. Bien sûr, ils sont également valables pour les scripts Shell, mais les gens s'attendent à ce genre de problèmes dans les scripts Shell. Mais les programmes C ne sont généralement pas censés avoir ce type de problème de sécurité, ce qui le rend plus dangereux.

Mais, à mon avis, les plus grandes inquiétudes concernent la manière dont popen interagira avec le reste de votre programme. popen doit créer un processus enfant, lire sa sortie et collecter son état de sortie. En attendant, le stderr de ce processus sera connecté au même stderr que votre programme, ce qui peut entraîner une sortie confuse, et son stdin sera le même que votre programme, ce qui pourrait entraîner d'autres problèmes intéressants. Vous pouvez résoudre ce problème en incluant </dev/null 2>/dev/null dans la chaîne que vous passez à popen car elle est interprétée par le Shell.

Et popen crée un processus enfant. Si vous faites quoi que ce soit avec la gestion des signaux ou les processus de forking vous-même, vous risquez de recevoir des signaux SIGCHLD étranges. Vos appels à wait peuvent interagir étrangement avec popen et éventuellement créer d'étranges conditions de concurrence.

Les problèmes de sécurité et de portabilité sont bien sûr là. Comme pour les scripts Shell ou tout ce qui démarre d'autres exécutables sur le système. Et vous devez faire attention à ce que les utilisateurs de votre programme ne puissent pas obtenir de méta-charcaters Shell dans la chaîne que vous passez dans popen car cette chaîne est donnée directement à sh avec sh -c <string from popen as a single argument>.

Mais je ne pense pas qu'ils soient la raison pour laquelle il est étrange de voir un programme C utilisant popen. La raison pour laquelle cela est étrange est que C est généralement un langage de bas niveau et que popen n'est pas de bas niveau. Et parce que l'utilisation de popen impose des contraintes de conception à votre programme car il interagira étrangement avec les entrées et sorties standard de votre programme et vous compliquera la gestion de processus ou la gestion des signaux. Et parce que les programmes C ne sont généralement pas censés avoir des dépendances sur les exécutables externes.

7
Omnifarious

Votre programme peut être sujet au piratage, etc. Une façon de vous protéger de ce type d'activité consiste à créer une copie de l'environnement de votre machine actuelle et à exécuter votre programme à l'aide d'un système appelé chroot.

Du point de vue de votre programme, son exécution dans un environnement normal, d'un point de vue de la sécurité, si quelqu'un casse votre programme, il n'a accès qu'aux éléments que vous avez fournis lorsque vous avez fait la copie.

Une telle configuration est appelée une prison chroot pour plus de détails, voir prison chroot .

Il est normalement utilisé pour configurer des serveurs accessibles au public, etc.

0
Dave