Não importa se é o melhor brinquedo do parquinho ou a melhor vaga para estacionar no Supermercado. Nossa vida é uma eterna disputa por recursos. E na Ciência da Computação não é muito diferente.

No artigo sobre o Jantar dos Filósofos, eu expliquei como acontece a disputa de processos que desejam usar recursos do Computador que são impossíveis de serem utilizados ao mesmo tempo (a impressora, por exemplo).

Neste artigo de agora, irei demonstrar como acontecem as vulnerabilidades de Condições de Corrida que envolvem recursos que podem ser utilizados por vários processos ao mesmo tempo.

 

Introdução

Condições de corrida ocorrem quando há dois ou mais processos disputando um recurso compartilhado e a ordem das ações nessa disputa não é controlada corretamente. Gerando diversos problemas de sincronização.

Para ilustrar, irei usar um exemplo de Transferência Bancária puramente didático e abstrair os diversos detalhes técnicos envolvidos em uma operação real.

Imagine que Alice e Bob possuem uma conta conjunta com o saldo de R$ 100,00 e pretendem transferir, cada um, R$ 50,00 para o seu filho João. Em um cenário perfeito e sincronizado, a operação acontece nas seguintes etapas:

Etapa A – Alice aperta o botão transferir e o seu banco recupera o saldo da Conta (R$100,00) para saber se ela está apta.

Etapa B – Logo após, o Banco de Alice envia o crédito de R$ 50,00 para o banco de João.

Imagem 1: Desenho de uma mulher, com uma seta apontando para um prédio preto que está a sua direita solicitando a transferência de R$ 50,00 para o João. O prédio diz que o saldo da mulher é de R$ 100,00 e fará a transferência. Este prédio preto aponta para um outro prédio que possui um garoto à sua direita, transferindo R$ 50,00.

Etapa C – O Banco do João confirma que recebeu os R$ 50,00.

Etapa D – O Banco da Alice atualiza saldo da Conta Conjunta (R$ 50,00).

Imagem 2: prédio branco com uma seta apontando para o prédio branco à sua direita informando que recebeu os 50 reais e que o banco preto pode atualizar seu saldo, agora de R$ 50,00.

O mesmo fluxo acontece com Bob logo depois:

Etapa A – Bob aperta o botão transferir e o seu banco recupera o saldo da Conta Conjunta (R$50,00) para saber se ele está apto a transferir.

Etapa B – Logo depois o banco transfere R$ 50,00 para o Banco de João

Imagem 3: Desenho de um homem, com uma seta apontando para um prédio preto que está a sua direita. O homem diz ao banco que quer transferir R$ 50,00 para o João. O prédio diz que fará a transferência pois o saldo é de R$ 50,00. Este prédio preto aponta para um outro prédio que possui o garoto à sua direita, o qual recebe mais R$ 50,00.

Etapa C – O Banco do João confirma que recebeu mais R$50,00.

Etapa D – O Banco do Bob atualiza o Saldo da Conta que ele possui com Alice (R$ 0,00).

Imagem 4: prédio branco com uma seta apontando para o prédio branco à sua direita informando que recebeu os 50 reais e que o banco preto pode atualizar seu saldo. O saldo do homem e da mulher é atualizado para R$ 0,00.

 

O Problema

Todos nós sabemos que a vida real é bem mais caótica e pode acontecer um cenário de Alice e Bob apertarem o botão de transferir ao mesmo tempo.

Dessa forma, as operações de Leitura dos dados (recuperação de saldo) serão realizadas de forma paralela. Ou seja, ao mesmo tempo. E ambas vão recuperar um saldo de R$100,00 na conta compartilhada entre os dois.

Imagem 5: Desenho de um prédio preto ao centro com um homem à esquerda e uma mulher à direita. Cada um deles possui uma seta apontando para o prédio do centro. Cada um solicita a transferência de R$ 50,00. Como a solicitação é simultânea, o banco responde aos dois, em separado, que fará a transferência pois o saldo é de R$ 100,00.

 Só que a operação de retorno, é de escrita (atualização de saldo após a confirmação) e, como ela não é possível de ser feita de forma simultânea, temos a nossa infeliz condição de corrida.

Imagem 6: Desenho de um prédio preto ao centro com um prédio branco à esquerda e um prédio branco à direita. Cada um deles possui uma seta apontando para o prédio do centro informando que recebeu os R$ 50,00. O banco do centro responde a ambos os bancos que irá atualizar o saldo de R$ 100,00 para R$ 50,00.

Neste caso específico, independente de qual for a confirmação vencedora (de Alice ou de Bob) o resultado final será desastroso para o banco, pois a conta conjunta dos dois irá terminar com R$50,00, ao invés de R$0,00.

 

Mitigação do Problema

O bloqueio de certos recursos, para que eles temporariamente se tornem exclusivos, elimina o risco de condições de corrida comprometerem todo o sistema. Esta trava pode ser feita utilizando-se de dois recursos clássicos do mundo dos Sistemas Distribuídos: Regiões de Exclusão Mútua ou Semáforos.

Irei exemplificar apenas os semáforos, mas os conceitos são bastante parecidos.

Imagem 7: Desenho de um homem com uma seta apontando para um semáforo que está à sua direita pedindo para transferir R$ 50,00. Este homem está em uma área sinalizada pela cor verde. Dentro dessa mesma área há um prédio, representando um banco que diz que irá fazer a transferência pois o sinal está verde para o homem. Na outra área, sinalizada pela cor vermelha, uma mulher informa que quando o sinal estiver verde também quer transferir 50 reais.

Como o próprio nome diz, um semáforo irá sinalizar aos envolvidos que alguém está utilizando exclusivamente, um recurso que antes era disponível a todos. No caso do nosso exemplo, o saldo da conta será lido apenas por Bob ou por Alice. E só estará disponível novamente quando toda a operação de transferência for realizada.

Imagem 8: Imagem de um semáforo com uma seta apontando para um homem. Esta seta, o homem e um prédio estão numa mesma área, sinalizada pela cor verde. O banco está dando a confirmação de que a transferência foi concluída. Numa outra área sinalizada pela cor vermelha, há o desenho de uma mulher informando estar aguardando.

Assim que a transferência de Bob é concluída, ele é bloqueado para acessar o saldo da Conta Conjunta e o Semáforo libera Alice para ela fazer a sua operação.

Imagem 9: Imagem de um homem dizendo que está bloqueado. Este homem está numa área sinalizada pela cor vermelha. Na outra área, sinalizada pela cor verde, um semáforo informa a uma mulher que os 50 reais serão transferidos.

 Após a conclusão da transferência de Alice ser concluída, o saldo está disponível para os dois novamente. E desta vez vai estar correto.

 

Um triste exemplo de Condição de Corrida no Mundo Real

Apesar de ser um conceito bastante antigo (em 1954 David A. Huffman o já abordava em sua tese de Doutorado sobre Circuitos Lógicos) e de não ser uma exclusividade do mundo da Programação Concorrente (pode ocorrer em Redes de Comunicação e Bancos de Dados) a vulnerabilidade de Condição de Corrida já foi responsável por grandes prejuízos financeiros e até por perdas de vidas humanas ao longo da história.

No início dos anos 80, a Atomic Energy of Canada Limited (AECL) um Laboratório Canadense de Energia Nuclear lançou no mercado uma nova versão de sua máquina de tratamento de radioterapia: o Therac-25. Esse novo dispositivo possuía atualizações significativas em relação aos antigos Therac-6 e Therac-20.

Basicamente, o Therac-25 tinha 3 Modos de operação que podiam ser alterados pelo operador: “Modo Raio-x”, “Modo Elétron” e “Modo Raio de Luz”. No Modo Raio-X, um magneto posicionava um filtro entre o paciente e o feixe de elétrons irradiado pelo aparelho, nos outros modos esse filtro não era necessário.

Na teoria, essa troca de estados (Feixe com Filtro e sem Filtro) funcionava de forma fluida e perfeitamente sincronizada.

Mas na prática, uma Condição de Corrida intermitente, gerava um atraso mortal de 8 segundos na movimentação do magneto. E infelizmente, uma dose de radiação sem filtros era bombardeada ao paciente durante as sessões.

Após diversas denúncias e uma investigação infelizmente tardia, foram registrados 6 graves incidentes provocados pelo Therac-25 sendo 4 mortes diretamente vinculadas a este problema no Software.

É importante ressaltar que essa tragédia aconteceu há 40 anos atrás e seria praticamente impossível de acontecer novamente. Pois os diversos erros de Engenharia de Software na construção desse aparelho tão crítico (apenas um programador estava envolvido na construção do código, troca de travamentos físicos por travamentos de Software, alertas confusos e com diversos falsos-positivos…) jamais seriam aceitos pelas Empresas Homologadoras de Aparelhos de Radioterapia de hoje.

 

Conclusão

Atualmente, as falhas de condição de corrida estão entre as 25 vulnerabilidades de Software mais perigosas, mas  por não serem simples de se reproduzir, são objeto de estudo de pesquisadores de Segurança Cibernética mais experientes e em ambientes controlados.

Nos raros casos em que conseguem ser reproduzidas artificialmente, garantem prestígio e um bom retorno financeiro para os envolvidos. Como exemplo, o Grupo Francês Synacktiv’s, que utilizou uma variante de condição de Corrida chamada TOCTOU (Time- -To-Check to Time-To-Use) para hackear completamente um veículo elétrico da marca Tesla em um Bug Bounty promovido pela Marca.

 

Caso tenha interesse em saber mais detalhes sobre o assunto, seguem abaixo os links de Biografia complementar.

Um abraço e obrigado pela leitura.

https://en.wikipedia.org/wiki/Race_condition

https://www.youtube.com/watch?v=MqnpIwN7dz0

https://www.youtube.com/watch?v=y73bgQNluBk