cfsolve 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +10 -9
  4. data/UPSTREAM.md +6 -6
  5. data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  6. data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  7. data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
  8. data/lib/cfsolve/version.rb +2 -2
  9. data/vendor/cfsolve/README.md +0 -35
  10. data/vendor/cfsolve/examples/build_profile.rs +17 -15
  11. data/vendor/cfsolve/examples/capture_session.rs +37 -0
  12. data/vendor/cfsolve/examples/check_answers.rs +66 -0
  13. data/vendor/cfsolve/examples/compare_session.rs +190 -0
  14. data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
  15. data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  16. data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  17. data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
  18. data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
  19. data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
  20. data/vendor/cfsolve/src/capture/profile.rs +7 -18
  21. data/vendor/cfsolve/src/capture/recorded.rs +42 -7
  22. data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
  23. data/vendor/cfsolve/src/challenge/installed.rs +122 -271
  24. data/vendor/cfsolve/src/challenge/payload.rs +107 -5
  25. data/vendor/cfsolve/src/challenge/state.rs +98 -16
  26. data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
  27. data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
  28. data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
  29. data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
  30. data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
  31. data/vendor/cfsolve/src/http/client.rs +11 -0
  32. data/vendor/cfsolve/src/js/ast.rs +12 -3
  33. data/vendor/cfsolve/src/js/objects.rs +9 -2
  34. data/vendor/cfsolve/src/listing/fold.rs +0 -83
  35. data/vendor/cfsolve/src/listing/query.rs +25 -244
  36. data/vendor/cfsolve/src/listing.rs +0 -1
  37. data/vendor/cfsolve/src/session/initial.rs +14 -5
  38. data/vendor/cfsolve/src/session/probes.rs +160 -125
  39. data/vendor/cfsolve/src/session/timing.rs +58 -53
  40. data/vendor/cfsolve/src/session/widget.rs +5 -6
  41. data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
  42. data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
  43. data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
  44. data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
  45. data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
  46. data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
  47. data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
  48. data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
  49. data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
  50. data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
  51. data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
  52. data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
  53. data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
  54. data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
  55. data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
  56. data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
  57. data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
  58. data/vendor/cfsolve/src/stage2/probes.rs +0 -1
  59. data/vendor/cfsolve/src/stage2/records.rs +4 -13
  60. data/vendor/cfsolve/src/stage2/reference.json +1 -1
  61. data/vendor/cfsolve/src/stage2/reference.rs +98 -100
  62. data/vendor/cfsolve/src/stage2/registry.rs +41 -34
  63. data/vendor/cfsolve/src/stage2/replay.rs +182 -254
  64. data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
  65. data/vendor/cfsolve/src/stage2/trace.rs +18 -0
  66. data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
  67. data/vendor/cfsolve/src/stage2.rs +2 -1
  68. data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
  69. data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
  70. data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
  71. data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
  72. data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
  73. data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
  74. data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
  75. data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
  76. data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
  77. data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
  78. data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
  79. data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
  80. data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
  81. data/vendor/cfsolve/src/vm/interpret.rs +7 -2
  82. data/vendor/cfsolve/src/vm/machine.rs +6 -2
  83. data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
  84. data/vendor/cfsolve/src/vm/session.rs +36 -14
  85. data/vendor/cfsolve/src/vm/value.rs +13 -0
  86. data/vendor/cfsolve/src/vm.rs +1 -0
  87. data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
  88. metadata +13 -6
  89. data/vendor/cfsolve/src/listing/normalize.rs +0 -264
  90. data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
  91. data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
  92. data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
  93. data/vendor/cfsolve/src/stage2/session_names.rs +0 -470
@@ -1,8 +1,9 @@
1
1
  //! Queries value sources, function reachability, and common program reads such as `Date.now()`,
2
2
  //! challenge options, and record timestamps.
3
3
  //!
4
- //! [`Index::assignment`] finds the nearest earlier instruction assigning a register and
5
- //! returns its first write, resolving temporaries through the path that computed them.
4
+ //! [`Index::register_before`] finds the nearest earlier instruction assigning a register and
5
+ //! returns the first write from any of its paths. [`Index::assignment`] resolves each write
6
+ //! through its own path, including path-local temporaries.
6
7
 
7
8
  use std::collections::{BTreeMap, BTreeSet};
8
9
 
@@ -10,9 +11,7 @@ use anyhow::{Context, Result, ensure};
10
11
  use swc_core::ecma::ast::BinaryOp;
11
12
 
12
13
  use super::Index;
13
- use super::fold::{Frame, constant};
14
14
  use crate::candidates;
15
- use crate::js::JsValue;
16
15
  use crate::vm::{Constant, Effect, Instruction, Listing, Term};
17
16
 
18
17
  /// A property write, `object.key = value` or `object[key] = value`. Dynamic keys resolve to the
@@ -56,55 +55,6 @@ pub(crate) fn walk(
56
55
  const DEREFERENCE_FUEL: usize = 8;
57
56
 
58
57
  impl Index<'_> {
59
- /// Recovers arrays populated through numeric stores, rejecting holes and conflicting writes.
60
- pub(crate) fn string_arrays(&self, entry: u32) -> Vec<Vec<String>> {
61
- let mut arrays = BTreeMap::<u32, BTreeMap<usize, String>>::new();
62
- let mut invalid = BTreeSet::new();
63
- for pc in self.reachable(entry) {
64
- let Some(instruction) = self.listing().instructions.get(&pc) else {
65
- continue;
66
- };
67
- for effect in instruction.effects() {
68
- let Effect::SetProperty { object, key, value } = effect else {
69
- continue;
70
- };
71
- let Some((origin, Term::Array(initial))) = self.dereference(pc, object) else {
72
- continue;
73
- };
74
- if !initial.is_empty() {
75
- continue;
76
- }
77
- let Some((_, Term::Const(Constant::Number(index)))) = self.dereference(pc, key)
78
- else {
79
- invalid.insert(origin);
80
- continue;
81
- };
82
- let Some(value) = self.text(pc, value) else {
83
- invalid.insert(origin);
84
- continue;
85
- };
86
- if !index.is_finite() || index < 0.0 || index.fract() != 0.0 {
87
- invalid.insert(origin);
88
- continue;
89
- }
90
- let array = arrays.entry(origin).or_default();
91
- if array
92
- .insert(index as usize, value.clone())
93
- .is_some_and(|old| old != value)
94
- {
95
- invalid.insert(origin);
96
- }
97
- }
98
- }
99
- arrays
100
- .into_iter()
101
- .filter(|(origin, array)| {
102
- !invalid.contains(origin) && array.keys().copied().eq(0..array.len())
103
- })
104
- .map(|(_, array)| array.into_values().collect())
105
- .collect()
106
- }
107
-
108
58
  /// Caches property writes in program-counter order. Resolves dynamic keys through the
109
59
  /// nearest earlier register assignment and skips stores whose keys are not known strings.
110
60
  #[must_use]
@@ -118,18 +68,22 @@ impl Index<'_> {
118
68
  let Term::Reg(object) = **object else {
119
69
  continue;
120
70
  };
121
- let Some(key) = self.text(pc, key) else {
71
+ let key = match &**key {
72
+ Term::Reg(register) => self.register_before(pc, *register),
73
+ key => Some(key),
74
+ };
75
+ let Some(Term::Const(Constant::String(key))) = key else {
122
76
  continue;
123
77
  };
124
78
  if stores.last().is_some_and(|store: &Store| {
125
- store.pc == pc && store.object == object && store.key == key
79
+ store.pc == pc && store.object == object && store.key == *key
126
80
  }) {
127
81
  continue;
128
82
  }
129
83
  stores.push(Store {
130
84
  pc,
131
85
  object,
132
- key,
86
+ key: key.clone(),
133
87
  value: (**value).clone(),
134
88
  });
135
89
  }
@@ -161,6 +115,18 @@ impl Index<'_> {
161
115
  reached
162
116
  }
163
117
 
118
+ /// Returns the first write to `register` on any path of the nearest assigning instruction
119
+ /// before `pc`.
120
+ fn register_before(&self, pc: u32, register: u8) -> Option<&Term> {
121
+ let at = self.assigning_before(pc, register)?;
122
+ self.listing().instructions[&at]
123
+ .effects()
124
+ .find_map(|effect| match effect {
125
+ Effect::Set(target, value) if *target == register => Some(value),
126
+ _ => None,
127
+ })
128
+ }
129
+
164
130
  /// Finds the nearest assignment to `register` before `before`, retaining its program
165
131
  /// counter so operands resolve at assignment time. Resolves temporaries only through the
166
132
  /// path that computed them; missing path values leave the assignment unknown.
@@ -205,56 +171,10 @@ impl Index<'_> {
205
171
  /// The string `term` holds at `pc`.
206
172
  #[must_use]
207
173
  pub(crate) fn text(&self, pc: u32, term: &Term) -> Option<String> {
208
- self.literal(pc, term, 16)?.as_str().map(str::to_owned)
209
- }
210
-
211
- /// Resolves the small constant expressions left in an inline string decoder when a
212
- /// control-flow join prevents whole-program folding. Every operand must be a literal.
213
- fn literal(&self, pc: u32, term: &Term, fuel: usize) -> Option<JsValue> {
214
- if fuel == 0 {
215
- return None;
174
+ match self.dereference(pc, term)? {
175
+ (_, Term::Const(Constant::String(text))) => Some(text),
176
+ _ => None,
216
177
  }
217
- let (pc, term) = self.dereference(pc, term)?;
218
- let literal = |term: &Term| -> Option<Term> {
219
- Some(Term::Const(match self.literal(pc, term, fuel - 1)? {
220
- JsValue::String(text) => Constant::String(text),
221
- JsValue::Number(number) => Constant::Number(number),
222
- JsValue::Bool(value) => Constant::Bool(value),
223
- JsValue::Null => Constant::Null,
224
- JsValue::Undefined => Constant::Undefined,
225
- _ => return None,
226
- }))
227
- };
228
- let resolved = match term {
229
- Term::Const(_) => term,
230
- Term::Binary(op, left, right) => {
231
- Term::Binary(op, Box::new(literal(&left)?), Box::new(literal(&right)?))
232
- }
233
- Term::Call {
234
- callee,
235
- this,
236
- arguments,
237
- } => {
238
- let (at, Term::Get(object, method)) = self.dereference(pc, &callee)? else {
239
- return None;
240
- };
241
- let object = self.literal(at, &object, fuel - 1)?.as_str()?.to_owned();
242
- let method = self.literal(at, &method, fuel - 1)?.as_str()?.to_owned();
243
- Term::Call {
244
- callee: Box::new(Term::Get(
245
- Box::new(Term::Const(Constant::String(object))),
246
- Box::new(Term::Const(Constant::String(method))),
247
- )),
248
- this: match this {
249
- Some(this) => Some(Box::new(literal(&this)?)),
250
- None => None,
251
- },
252
- arguments: arguments.iter().map(literal).collect::<Option<_>>()?,
253
- }
254
- }
255
- _ => return None,
256
- };
257
- constant(&Frame::<()>::default(), &resolved)
258
178
  }
259
179
 
260
180
  /// Resolves `term` to an `object[key]` read with a string key.
@@ -356,142 +276,3 @@ impl Index<'_> {
356
276
  Ok([started, finished, duration])
357
277
  }
358
278
  }
359
-
360
- #[cfg(test)]
361
- mod tests {
362
- use super::*;
363
- use crate::vm::{Exit, Path};
364
-
365
- fn text(value: &str) -> Term {
366
- Term::Const(Constant::String(value.into()))
367
- }
368
- fn number(value: f64) -> Term {
369
- Term::Const(Constant::Number(value))
370
- }
371
- fn call(object: Term, method: &str, arguments: Vec<Term>) -> Term {
372
- Term::Call {
373
- callee: Box::new(Term::Get(Box::new(object), Box::new(text(method)))),
374
- this: None,
375
- arguments,
376
- }
377
- }
378
- fn store(object: u8, key: Term, value: Term) -> Effect {
379
- Effect::SetProperty {
380
- object: Box::new(Term::Reg(object)),
381
- key: Box::new(key),
382
- value: Box::new(value),
383
- }
384
- }
385
- fn listing(effects: Vec<Effect>) -> Listing {
386
- let count = effects.len();
387
- let mut listing = Listing::default();
388
- for (pc, effect) in effects.into_iter().enumerate() {
389
- listing.instructions.insert(
390
- pc as u32,
391
- Instruction {
392
- pc: pc as u32,
393
- key: 0,
394
- opcode: 0,
395
- paths: vec![Path {
396
- conditions: vec![],
397
- effects: vec![effect],
398
- exit: if pc + 1 == count {
399
- Exit::Return
400
- } else {
401
- Exit::Next {
402
- pc: pc as u32 + 1,
403
- key: 0,
404
- }
405
- },
406
- }],
407
- },
408
- );
409
- }
410
- listing
411
- }
412
-
413
- #[test]
414
- fn resolves_stored_keys_through_inline_string_decoders() {
415
- let listing = listing(vec![
416
- Effect::Set(1, text("\0\u{6}Field7padding")),
417
- Effect::Set(2, number(2.0)),
418
- Effect::Set(3, number(256.0)),
419
- Effect::Set(4, call(Term::Reg(1), "charCodeAt", vec![number(0.0)])),
420
- Effect::Set(5, call(Term::Reg(1), "charCodeAt", vec![number(1.0)])),
421
- Effect::Set(
422
- 4,
423
- Term::Binary(
424
- BinaryOp::Mul,
425
- Box::new(Term::Reg(4)),
426
- Box::new(Term::Reg(3)),
427
- ),
428
- ),
429
- Effect::Set(
430
- 4,
431
- Term::Binary(
432
- BinaryOp::Add,
433
- Box::new(Term::Reg(4)),
434
- Box::new(Term::Reg(5)),
435
- ),
436
- ),
437
- Effect::Set(
438
- 4,
439
- Term::Binary(
440
- BinaryOp::Add,
441
- Box::new(Term::Reg(4)),
442
- Box::new(Term::Reg(2)),
443
- ),
444
- ),
445
- Effect::Set(
446
- 1,
447
- call(Term::Reg(1), "slice", vec![Term::Reg(2), Term::Reg(4)]),
448
- ),
449
- store(9, Term::Reg(1), number(1.0)),
450
- store(
451
- 9,
452
- call(Term::Reg(99), "slice", vec![number(0.0), number(1.0)]),
453
- number(2.0),
454
- ),
455
- ]);
456
- let index = Index::new(&listing);
457
- assert_eq!(index.stores().len(), 1);
458
- assert_eq!(index.stores()[0].key, "Field7");
459
- }
460
-
461
- #[test]
462
- fn string_methods_use_the_explicit_receiver() {
463
- let mut term = call(text("other"), "slice", vec![number(0.0), number(3.0)]);
464
- let Term::Call { this, .. } = &mut term else {
465
- unreachable!()
466
- };
467
- *this = Some(Box::new(text("fresh")));
468
- let program = listing(vec![]);
469
- let index = Index::new(&program);
470
- assert_eq!(index.text(0, &term).as_deref(), Some("fre"));
471
- let Term::Call { this, .. } = &mut term else {
472
- unreachable!()
473
- };
474
- *this = Some(Box::new(Term::Reg(99)));
475
- assert_eq!(index.text(0, &term), None);
476
- }
477
-
478
- #[test]
479
- fn collects_complete_string_arrays_and_rejects_uncertain_contents() {
480
- let listing = listing(vec![
481
- Effect::Set(1, Term::Array(vec![])),
482
- store(1, number(1.0), text("second")),
483
- store(1, number(0.0), text("first")),
484
- Effect::Set(2, Term::Array(vec![])),
485
- store(2, number(1.0), text("gap")),
486
- Effect::Set(3, Term::Array(vec![])),
487
- store(3, number(0.0), text("before")),
488
- store(3, number(0.0), text("after")),
489
- Effect::Set(4, Term::Array(vec![])),
490
- store(4, Term::Reg(99), text("unknown index")),
491
- ]);
492
- assert_eq!(
493
- Index::new(&listing).string_arrays(0),
494
- [vec!["first", "second"]]
495
- );
496
- }
497
- }
@@ -7,7 +7,6 @@
7
7
 
8
8
  pub(crate) mod fold;
9
9
  mod index;
10
- pub(crate) mod normalize;
11
10
  pub(crate) mod query;
12
11
  mod strings;
13
12
 
@@ -1,6 +1,8 @@
1
1
  //! Prepares the first payload: renders the embedding page, fetches and parses the iframe, and
2
2
  //! merges page parameters with iframe state.
3
3
 
4
+ use std::mem;
5
+
4
6
  use anyhow::{Context, Result};
5
7
  use url::Url;
6
8
 
@@ -20,6 +22,7 @@ use crate::resource_timing::LoadTiming;
20
22
  use crate::script::Source;
21
23
  use crate::stage2::call_stack::{self, CaseOpcodes, ScriptPosition};
22
24
  use crate::stage2::payload::Environment;
25
+ use crate::stage2::replay::Globals;
23
26
  use crate::{deep_stack, performance_clock};
24
27
 
25
28
  /// Default loader URL for pages that load Turnstile dynamically without a script tag.
@@ -68,6 +71,8 @@ impl Fetched {
68
71
 
69
72
  /// The evaluated first-payload template, before sender fields, and state for later payloads.
70
73
  pub(crate) struct FirstPayload {
74
+ /// Signatures of the session-named globals the initial program installs.
75
+ pub audit_globals: Globals,
71
76
  /// The extracted program with fully populated request options.
72
77
  pub program: ExtractedProgram,
73
78
  pub state: ScriptState,
@@ -94,11 +99,11 @@ impl FirstPayload {
94
99
  options_artifact: &str,
95
100
  complete: impl FnOnce(&mut Options) -> (Option<&'mark str>, i64),
96
101
  ) -> Result<Self> {
97
- // Keep the extracted facts available to later stages; request state is a separate copy.
98
- let installed = &program.installed;
102
+ let installed = mem::take(&mut program.installed);
103
+ let audit_globals = Globals::installed(&installed);
99
104
  let fields = &program.payload_fields;
100
105
  let options = &mut program.request.options;
101
- options.extend(installed.options.clone());
106
+ options.extend(installed.options);
102
107
  if let Some(log) = &state.post_log {
103
108
  log.start(options);
104
109
  }
@@ -110,10 +115,11 @@ impl FirstPayload {
110
115
  intervals,
111
116
  ));
112
117
  sink.json(options_artifact, options)?;
113
- let window = installed.window.clone();
118
+ let window = installed.window;
114
119
  let globals = payload::infer_globals(fields, &state, options, &window);
115
120
  let plaintext = payload::build(fields, options, &globals)?;
116
121
  Ok(Self {
122
+ audit_globals,
117
123
  program,
118
124
  state,
119
125
  window,
@@ -126,6 +132,7 @@ impl FirstPayload {
126
132
 
127
133
  /// Iframe state after merging page parameters, shared by the first and later payloads.
128
134
  pub(crate) struct Merged {
135
+ pub audit_globals: Globals,
129
136
  /// The first payload, which the probe payload extends.
130
137
  pub first: Payload,
131
138
  pub state: ScriptState,
@@ -214,7 +221,8 @@ pub(crate) async fn render_page(
214
221
  sink,
215
222
  rendered,
216
223
  script,
217
- page.layout.clone(),
224
+ page.layout
225
+ .placed(page.layout.embedding_pages.get(document.url.as_str())),
218
226
  document.referrer_policy,
219
227
  Embedding::Site,
220
228
  ))
@@ -386,6 +394,7 @@ pub(crate) fn merge(
386
394
  ));
387
395
  sink.json("payload.json", &first)?;
388
396
  let merged = Merged {
397
+ audit_globals: built.audit_globals,
389
398
  first,
390
399
  state: built.state,
391
400
  window: built.window,