Skip to content

v1.4.0: MYSQL_SYNC, diagnóstico de TLS, limites ajustáveis, ${ENV} e duas correções de segurança - #59

Merged
NullSablex merged 11 commits into
masterfrom
feat/backlog-2026-09
Sep 26, 2026
Merged

NullSablex merged 11 commits into
masterfrom
feat/backlog-2026-09

Conversation

@NullSablex

@NullSablex NullSablex commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Release 1.4.0. Aditiva do lado Pawn — nenhuma native foi removida ou renomeada, então gamemodes da 1.3.0 compilam sem mudança. Uma mudança de comportamento exige atenção, descrita logo abaixo.

Atenção antes de atualizar

OnQueryError entregava os cinco argumentos em ordem inversa. O forward é declarado OnQueryError(errorid, error[], callback[], query[], connId), mas o gamemode recebia o id da conexão em errorid, o texto da query em error, a mensagem de erro em query e o código do MySQL em connId.

A causa está no despacho: o exec_public! empilha os argumentos ao contrário, que é a convenção da pilha do AMX, então a ordem listada na chamada é a ordem em que o public recebe — e o código listava começando pelo fim, como se precisasse inverter manualmente. O defeito existe desde a 1.0.0.

Quem trata esse forward vinha lendo os campos trocados e precisa conferir o handler.

Novidades

MYSQL_SYNC — bloqueio explícito, por chamada

O padrão continua sendo não-bloqueante. Escrever MYSQL_SYNC no lugar do nome do callback faz aquela chamada esperar o banco e deixa o resultado legível na linha seguinte:

mysql_query(g_sql, "SELECT COUNT(*) AS n FROM accounts", MYSQL_SYNC);
new total = cache_get_value_name_int(0, "n");

É um valor e não uma native porque uma mysql_query_sync termina copiada para dentro de OnPlayerUpdate; um valor escrito na chamada declara a intenção onde o custo é pago. Também não podia ser um argumento novo: o Pawn recusa qualquer parâmetro depois de {Float,_}:..., e colocá-lo antes do callback quebraria todas as chamadas existentes.

Três regras sustentam o recurso:

  • Recusado dentro de um callback. Ali "cache corrente" já significa o do callback, e um resultado bloqueante ou o sombreia ou fica ilegível até o retorno.
  • Liberado no tick seguinte, a mesma garantia que o caminho assíncrono ganha do callback ao retornar. Não há liberação manual.
  • Recusado nas natives que não podem honrá-lo — prepared statements, transações, ORM e hashing de senha. Sem isso o valor passaria por nome de callback inexistente e a chamada viraria dispare-e-esqueça em silêncio.

Aceito em mysql_query, mysql_pquery e mysql_query_file.

Diagnóstico de TLS

mysql_tls_active(connId) e mysql_tls_cipher(connId, dest[], max_len) informam o que o handshake negociou, e não o que MYSQL_OPT_SSL pediu. O valor é capturado uma vez na abertura da conexão, então nenhuma das duas custa query. Sessão sem criptografia escreve cifra vazia e retorna false.

Limites de memória ajustáveis

mysql_limit_set(limit, value) e mysql_limit_get(limit) tornam configuráveis os três tetos que antes eram fixos: MYSQL_LIMIT_SAVED_CACHES (1024), MYSQL_LIMIT_RESULT_ROWS (100000) e MYSQL_LIMIT_ORM_STRING_LEN (4096). São globais porque a memória protegida é a do servidor. Um valor precisa ser positivo — 0 é recusado em vez de significar "ilimitado" — e baixar um teto preserva o que já está guardado.

${NAME} no arquivo de conexão

O mysql_connect_file já mantinha a senha fora do gamemode; agora o arquivo pode nomear o segredo em vez de guardá-lo:

password = ${MYSQL_PASSWORD}

Nome não definido expande para vazio e registra qual faltou, em vez de enviar o literal ao servidor e produzir um "access denied" enganoso. Apenas ${...} é referência, então senha com cifrão continua válida.

Outras correções

TLS em host de loopback rodava sem criptografia. MYSQL_OPT_SSL = 1 contra 127.0.0.1 reportava sucesso com Ssl_cipher vazio. O prefer_socket do driver, ligado por padrão, move uma conexão de loopback para o socket unix após o handshake, e ali nada é cifrado. Pedir TLS agora desliga essa otimização; sem TLS ela permanece.

rustls 0.23.43 → 0.23.45 (RUSTSEC-2026-0285). Mensagens de handshake do TLS 1.3 aceitas através da fronteira de nível de criptografia.

Erros do driver chegavam ao Pawn com estrutura interna do Rust. Uma tabela inexistente chegava como MySqlError { ERROR 1146 (42S02): ... }. Agora passa apenas a mensagem do servidor.

mysql_query_file falhava sem avisar quem chamou. Arquivo ilegível ou sem statements era apenas linha de log e retorno false, invisível para a forma sem callback. Agora também dispara OnQueryError, com o caminho no lugar da query e errorid 0.

Dependências e CI

  • rust-samp passa da tag do git para o crates.io, em 3.5.0.
  • mysql 28.0.0 → 28.0.2, mais uma rodada ampla de transitivas.
  • Dependabot passa a acompanhar dependências transitivas. Quase nada do que o plugin distribui está no Cargo.toml, então observar apenas o manifesto deixava correções de segurança passarem despercebidas.
  • A auditoria de segurança também roda semanalmente, abrindo issue em vez de reprovar um pull request sem relação.
  • tests/inc_natives.rs compara a lista do initialize_plugin! com o include nos dois sentidos, para que uma native registrada e não declarada falhe o build em vez de ficar inalcançável a partir do Pawn.

Documentação

O callback sempre foi opcional, mas isso estava dito em uma subseção no fim de uma página enquanto a abertura e todos os exemplos sugeriam o contrário. Da mesma forma, a ordenação FIFO vale para a entrega de callbacks e não para a execução — statements submetidos em sequência executam concorrentemente —, e a tabela comparativa recomendava justamente o uso que falha.

Ficaram registradas garantias que existiam apenas no código: mysql_connect bloqueia enquanto as queries não; TLS falha fechado; o caminho do mysql_query_file é relativo ao diretório do servidor; e coluna binária não trafega em string Pawn, o que torna VEC_ToText() e HEX() o caminho para VECTOR e BLOB.

Com o MYSQL_SYNC, as páginas deixaram de afirmar que tudo é assíncrono, sem passar a anunciar suporte a síncrono de forma genérica.

Verificação

Executado em SA-MP 0.3.7 com MariaDB 11.8, não apenas compilado:

  • 196 testes no alvo i686-unknown-linux-gnu
  • cargo clippy --all-targets -- -D warnings, cargo fmt --check e a verificação de codificação dos arquivos Pawn, todos limpos
  • mkdocs build --strict sem links quebrados
  • cargo audit sem vulnerabilidades; permanecem os dois avisos informativos já documentados no changelog

Changelog da 1.4.0 datado de 04/10/2026, cobrindo as 30 alterações desde a v1.3.0.

A 3.5.0 esta publicada no registro, entao a dependencia sai da tag do git
e passa a vir de la, com checksum no lockfile.

Corrige de quebra uma divergencia antiga: a tag `v3.5.0` declarava
`version = "3.4.0"` no proprio manifesto, e o `Cargo.lock` registrava
3.4.0 apontando para a tag v3.5.0.

O modulo `mainthread` da 3.5.0 nao e adotado ainda: o `post` recebe
closure sem parametro e nao ha rota para o estado do plugin a partir de
uma worker thread, entao o canal `mpsc` do `QueryManager` continua.
Uma native registrada no `initialize_plugin!` e nao declarada no include
compila sem erro e fica inalcancavel a partir do Pawn; o inverso falha so
quando alguem chama. Nenhum dos dois aparecia em build ou teste.

O teste novo compara as duas listas nos dois sentidos, a partir de
arquivos que qualquer build ja produz. Um segundo teste confere que o
include no estilo open.mp tem alias para toda native do base, que e o que
pega um deslize na conversao de nomes do build.rs.

O `SAMP_PAWN_INCLUDE` do SDK, que escreve as declaracoes de todas as
natives registradas quando o servidor sobe, fica documentado no
CONTRIBUTING como conferencia manual: precisa de servidor vivo, entao nao
serve para o CI.
`mysql_tls_active(connId)` e `mysql_tls_cipher(connId, dest[], max_len)`
informam o que o handshake negociou, e nao o que `MYSQL_OPT_SSL` pediu -
a opcao e o pedido, estas sao a resposta. O valor e lido uma vez no
`connect()`, na conexao que ja e aberta para detectar o `sql_mode`, entao
nenhuma das duas custa query. Sessao sem cifra escreve string vazia e
retorna false; o retorno separa esse caso do id inexistente.

A correcao que vem junto e mais grave. Com `MYSQL_OPT_SSL = 1` contra
`127.0.0.1`, `Ssl_cipher` voltava vazio: a conexao pedia TLS, nao
reportava erro e trafegava em texto claro.

A causa e o `prefer_socket` do driver, ligado por padrao. Depois do
handshake ele move uma conexao de loopback para o socket unix do
servidor, onde nada e cifrado - e as duas condicoes se encontram no
arranjo local mais comum que existe. Pedir TLS agora desliga essa
otimizacao; sem TLS ela permanece, que e onde ela ajuda.

Medido contra MariaDB 11.8: `TLS_AES_256_GCM_SHA384` onde antes havia
string vazia. Um teste fixa a armadilha, conferindo que o padrao do
driver casa as duas condicoes e que o que o `connect()` monta com TLS
nao as casa mais.
Os tres tetos que protegem a memoria do servidor estavam cravados no
codigo: 1024 caches salvos, 100000 linhas por resultado e 4096 bytes por
string do ORM. Numero fixo atende o caso comum e atrapalha os dois
extremos - o relatorio que le mesmo uma tabela grande, e a maquina
apertada que quer ser mais rigorosa.

`mysql_limit_set(limit, value)` e `mysql_limit_get(limit)`, com as
constantes MYSQL_LIMIT_SAVED_CACHES, MYSQL_LIMIT_RESULT_ROWS e
MYSQL_LIMIT_ORM_STRING_LEN. Globais, nao por conexao: a memoria
protegida e a do servidor, que todas compartilham.

Os valores ficam em `src/limits.rs` como atomicos porque o teto de linhas
e aplicado no `collect_result`, que roda em worker thread e nao tem rota
para o estado do plugin. A leitura acontece por linha, entao `Relaxed` da
conta.

Valor tem de ser positivo: zero e recusado em vez de virar "ilimitado",
ja que ilimitado e o estado que esses tetos existem para evitar. Baixar
um teto mantem o que ja esta guardado e recusa o proximo. Bater o teto de
linhas continua truncando com aviso, agora citando o teto vigente.
O `mysql_connect_file` ja tirava a senha do codigo do gamemode. Agora
`${NOME}` em qualquer valor e trocado pela variavel de ambiente, entao o
arquivo nomeia o segredo em vez de guarda-lo, e uma copia dele nao vale
nada sozinha.

Nome ausente expande para vazio e registra qual faltou: mandar o literal
`${MYSQL_PASSWORD}` ao servidor daria "access denied" e mandaria o
operador procurar no lugar errado. So `${...}` e referencia, entao senha
com cifrao continua funcionando, e a expansao vale em qualquer valor.

Junto vai uma correcao de mensagem: o caminho de conexao formatava o erro
do driver com o `Display` cru, entao `mysql_error()` devolvia
`Pool creation failed: MySqlError { ERROR 1045 (28000): ... }`. Passa a
usar o mesmo tratamento que o caminho de query ja tinha, entregando so a
mensagem do servidor.
Uma coluna VECTOR lida crua volta vazia para o Pawn, e o motivo nao e o
tipo: um float32 comeca com bytes zero, e string Pawn termina no primeiro
NUL. BLOB nao volta vazio, volta corrompido pela conversao para texto.
Binario nao trafega em string Pawn.

O caminho e converter no SQL: HEX() para blob, VEC_ToText() e
VEC_DISTANCE_*() para vetor, que chegam como string e float normais.
Verificado contra MariaDB 11.8 - nao ha nada a acrescentar no plugin.

A tabela de limites da mesma pagina dizia numeros fixos e agora aponta as
constantes MYSQL_LIMIT_*.
Tres lacunas, todas verificadas antes de virar texto:

1. `mysql_connect` bloqueia. A documentacao diz que toda query e
   nao-bloqueante, o que e verdade, mas abrir a conexao espera o
   handshake: cerca de 10 ms contra um banco local, 70 ms com TLS. A
   pagina de conexao passa a avisar que isso e aceitavel no
   OnGameModeInit e nao dentro de callback de jogador.

2. TLS falha fechado. Servidor que nao oferece criptografia faz o connect
   falhar, em vez de seguir em texto claro numa conexao que pediu cifra.

3. O caminho do `mysql_query_file` e relativo ao diretorio de trabalho do
   servidor, nao a `scriptfiles/`.

A tabela de limites da pagina de seguranca passa a dizer "padrao" onde
dizia "limite", com a ressalva de que da para estreitar e alargar, mas
nao remover.
O padrao continua nao-bloqueante. `MYSQL_SYNC` no lugar do nome do
callback faz aquela chamada esperar o banco, e o resultado e o cache
corrente na linha seguinte, sem forward e sem public.

Nao e native nova porque uma `mysql_query_sync` termina copiada para
dentro de callback de jogador; um valor escrito na chamada declara a
intencao onde o custo e pago. Tambem nao podia ser argumento novo: o Pawn
recusa parametro depois de `{Float,_}:...`, e coloca-lo antes do callback
quebraria toda chamada existente. O valor e uma string que nenhum public
pode ter, ja que identificador nao aceita `!`.

O resultado vive num campo proprio do CacheManager, e nao na pilha de
ativos: a pilha e balanceada por um push por callback e um pop na volta,
entao um push extra feito de dentro de um callback seria removido por ele
e deixaria orfa a entrada de baixo. A pilha tem prioridade de leitura
sobre esse campo, porque so esta cheia enquanto um callback roda e ali o
cache corrente tem de ser o do callback.

Sincrono dentro de um callback e recusado: o resultado ou sombrearia o
cache do callback ou ficaria ilegivel ate ele voltar, e as duas saidas
surpreendem. Os usos que o recurso atende - schema no arranque, migracao,
comando de console - acontecem fora de callback.

Suportado em `mysql_query`, `mysql_pquery` e `mysql_query_file`. Nas
demais e recusado com aviso, e nao ignorado: sem a checagem o valor
viraria nome de callback inexistente e a chamada seria dispare-e-esqueca
silenciosa. Nas de senha a recusa tem motivo proprio, ja que a assinatura
nao tem buffer de saida e o Argon2id e lento de proposito.

Verificado em servidor: `SELECT SLEEP(1)` sincrono travou 1002 ms, quatro
statements de schema em sequencia deram o resultado correto sem corrida,
erro devolveu false e disparou OnQueryError, e o caminho assincrono segue
intacto.
O resultado de um `MYSQL_SYNC` ficava no lugar ate outra chamada
bloqueante substituir ou alguem chamar `cache_unset_active()`. Num script
que faz uma unica chamada dessas no arranque, que e o uso previsto, isso
significa segurar o resultado pela vida do processo - e com o teto padrao
em 100 mil linhas pode ser bastante memoria parada.

Agora ele e liberado no inicio do tick seguinte. O servidor e
single-threaded, entao um public do Pawn roda inteiro dentro de um frame
e quem chamou ja retornou quando o proximo tick comeca. Isso da ao
caminho bloqueante a mesma garantia deterministica que o assincrono tem
do callback ao retornar.

A regra fica enunciavel numa frase: o resultado vale no frame em que foi
pedido; para guardar alem disso, `cache_save()`. Sem liberacao manual.

Verificado em servidor: no mesmo frame o resultado e legivel; num timer
200 ms depois nao ha cache ativo; e o que foi guardado com `cache_save()`
permanece.
Com o MYSQL_SYNC, "toda query e nao-bloqueante" deixou de ser verdade, e
anunciar "suporta sincrono" convidaria exatamente o uso que o recurso
dificulta de proposito. O texto passa a dizer as duas coisas juntas:
nao-bloqueante por padrao, bloqueante quando a chamada pede.

Paginas ajustadas: a abertura de queries, que agora lista os dois pontos
que bloqueiam - o connect e o MYSQL_SYNC - em vez de tratar o connect
como excecao unica; README, index, benchmark, CHECKLIST e a referencia da
API, que ganhou a constante.

O guia de migracao era o mais defasado: dizia que o `Cache:mysql_query`
do R41-4 foi removido por design, sem dar alternativa a quem vem de la
justamente atras da chamada bloqueante. Passa a mapear para
`mysql_query(connId, query, MYSQL_SYNC)`, com a ressalva de que e
recusado dentro de callback.
Bump do `Cargo.toml` para 1.4.0 e secao do changelog datada, cobrindo as
30 alteracoes desde a v1.3.0.

Minor porque do lado Pawn tudo e aditivo: nenhuma native foi removida ou
renomeada, entao gamemode da 1.3.0 compila sem mudanca. A excecao,
destacada no primeiro paragrafo da secao, e o `OnQueryError`, que passou
a entregar os argumentos na ordem declarada pelo forward - quem tratava
esse forward lia os campos trocados e precisa conferir o handler.

O changelog registra tambem as mudancas de dependencia (rust-samp 3.5.0
do crates.io, mysql 28.0.2, rustls 0.23.45 pelo RUSTSEC-2026-0285) e
atualiza o estado dos dois avisos informativos: a correcao do `lru` esta
mergeada no upstream desde 21/09 mas ainda nao lancada, e o `mysql`
continua exigindo a faixa 0.16.
@NullSablex
NullSablex merged commit f46d539 into master Sep 26, 2026
11 checks passed
@NullSablex
NullSablex deleted the feat/backlog-2026-09 branch September 26, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant