Gem hosted `rollback` / `remove` turn a redirected transitive gem into a top-level exact pin, because the restore looks for a blank line the rewriter never writes
Mantenedores costumam responder em até 1 dia
Ninguém assumiu esta issue ainda.
Avaliação
- Dificuldade
- 3/5
- Tempo estimado
- 1-2 dias
- Facilidade para iniciantes
- 78/100
Direção de pesquisa
Read crates/socket-patch-core/src/patch/redirect/mod.rs:5070 and crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:480,691, then run provably_transitive_needs_a_blank_line_before_the_block. Reproduce rollback and remove with an LF-terminated Gemfile and CHECKSUMS lock; done means both restore the Gemfile and lock byte-identically without creating a direct dependency, while existing blank-line behavior remains covered.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
When scan --mode hosted redirects a transitive gem, it appends a source "<patch registry>" do … end block to the end of the Gemfile. When the Gemfile already ends in a newline (the normal case), it writes no blank separator line. The upstream restore that rollback and remove share only treats a block as "the rewriter's append for a transitive gem" (Decl::Transitive) when a blank line precedes the block (provably_appended). Because the rewriter's own append never has one, the restore falls through to Decl::Direct. The transitive gem comes back as a new top-level declaration, gem "<name>", "<version>", and the lock gains a DEPENDENCIES entry <name> (= <version>).
This only affects converged locks, which means locks with a CHECKSUMS section (Bundler 2.6+ with checksums enabled, the Bundler 4 default). CHECKSUMS-less locks take the mixed-state path, which reads DEPENDENCIES and gets this right.
Impact
- After
rollback/remove, the project is not back on its upstream registry state, which is what CLI_CONTRACT.md's "Hosted unwind coverage" promises. It has a new direct dependency pinned to exactly the version that had the vulnerability. - That pin freezes the vulnerable version. In the repro,
bundle update rackkeepsrack 3.2.1after the rollback, while the byte-identical control upgrades torack 3.2.7. A user who removes the hosted patch to take the upstream fix can't get it until they find and delete a declaration they never wrote. - It's silent:
status: success,reverted: [pkg:gem/[email protected]], no warning.
Repro (Linux, Ruby 3.3.6, Bundler 4.0.17; real rubygems.org upstream, local mock for the patch API and the patch-registry compact index)
# Project: rack 3.2.1 is transitive via rackup; the Gemfile ends with a declaration line + "\n".
printf 'source "https://rubygems.org"\n\ngem "rackup", "2.2.1"\n' > Gemfile
bundle config set --local path vendor/bundle
bundle install # Bundler 4 writes CHECKSUMS
cp Gemfile Gemfile.pristine; cp Gemfile.lock lock.pristine; rm -rf vendor
socket-patch scan --mode hosted --json --yes --api-url $API --org test-org --api-token fake
# -> redirected 1, rewrittenFiles [Gemfile, Gemfile.lock]; block appended right after `gem "rackup"` (no blank line)
socket-patch rollback --json --yes --patch-server-url $API --api-url $API --org test-org --api-token fake
# -> status success, hosted.reverted [pkg:gem/[email protected]]
diff Gemfile.pristine Gemfile
# 3a4
# > gem "rack", "3.2.1"
diff lock.pristine Gemfile.lock
# 12a13
# > rack (= 3.2.1)
bundle update rack && grep ' rack (' Gemfile.lock # rack (3.2.1): stuck on the vulnerable version
Control: the same flow with the Gemfile ending in \n\n (one trailing blank line) restores byte-identically (Gemfile and lock), and bundle update rack moves to 3.2.7.
socket-patch remove pkg:gem/[email protected] gives the same result as rollback.
Expected vs actual
- Expected (CLI_CONTRACT.md, "Hosted unwind coverage", gem bullet): "the spec moves back into the upstream
GEMsection …, thesource "<patch registry>" do … endblock is undone … and theDEPENDENCIESpin loses its!". For a gem the rewriter appended because it was transitive, the code's own intent (Decl::Transitive: "Gone: the block was the rewriter's append for a transitive gem") is that the block and theDEPENDENCIESentry both go away. The documented "comes back as the exact pin" caveat covers a gem that had a declaration, not one the user never declared. - Actual: a new
gem "rack", "3.2.1"line and arack (= 3.2.1)DEPENDENCIES entry.
Matrix
| OS | Ruby | Bundler | Lock | Result |
|---|---|---|---|---|
| Linux | 3.3.6 | 4.0.17 | CHECKSUMS (default) | reproduces (rollback twice, remove once) |
| Linux | 3.3.6 | 2.6.9 | bundle lock --add-checksums |
reproduces |
| Linux | 3.3.6 | 4.0.17 | CHECKSUMS, Gemfile ends with a blank line | pass (byte-identical restore) |
| Linux | 3.3.6 | 2.6.9 | no CHECKSUMS (mixed state) | n/a: the lock isn't converged, and rollback can't see a Gemfile-only pin (documented) |
The logic is plain text processing with no OS dependency, so macOS and Windows should behave the same. I didn't bisect: the upstream restore is new in v5 (#277), and main 2463257 is the first commit that has it.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:5070: the transitive append islet sep = if gf.ends_with('\n') { "" } else { "\n" };, which leaves no blank line before the block.crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:480(provably_appended) and:691: the restore requires a blank line before the block to chooseDecl::Transitive. The unit testprovably_transitive_needs_a_blank_line_before_the_blockbuilds its "appended" fixture asgem "puma"\n\n{block}, a shape the rewriter never produces on an LF-terminated Gemfile.
One possible fix: have the rewriter's append write a blank separator, so its output is provably distinguishable from an in-place rewrite (which swallows the preceding blank lines). Hosted Gemfiles written by earlier runs would still need a fallback.
- Linguagem predominante
- Rust
- Estrelas
- 8
- Forks
- 0
- Merge médio
- 18h 4min
- PRs com merge (30d)
- 70
Preparar o ambiente
- Sem Dockerfile nem arquivo Docker Compose
- Sem modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de SocketDev/socket-patch
-
agent:triaged bug bughunt pm:composer priority:p2
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 90/100
SocketDev/socket-patch#515 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
agent:triaged bug bughunt pm:npm priority:p1
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
SocketDev/socket-patch#464 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
agent:triaged bug bughunt pm:npm priority:p1
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
SocketDev/socket-patch#433 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
agent:triaged bug bughunt pm:uv priority:p1
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
SocketDev/socket-patch#408 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
agent:triaged bug bughunt pm:yarn-berry priority:p1
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
SocketDev/socket-patch#370 · 2 comentários ·
Mantenedores costumam responder em até 1 dia
Todas as issues de SocketDev/socket-patch
Issues semelhantes
-
component:sight
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 84/100
agentic-os-org/ANOLISA#4115 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
A-io-database bug needs triage python
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 86/100
Mantenedores costumam responder em até 1 dia
-
Change output crossing a compactsize boundary leaves the fee slightly below the requested feerateAbertabug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
bitcoindevkit/bdk_wallet#578 ·
Mantenedores costumam responder em até 8 dias
-
`python.analysis` setting changes (e.g. `completeFunctionParens`) are not picked up until reloadAbertalanguage-server
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 86/100
Mantenedores costumam responder em até 2 dias
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 88/100
Mantenedores costumam responder em até 2 dias