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,11 +1,12 @@
1
1
  //! Explores feasible paths through an instruction handler.
2
2
 
3
- use std::mem;
3
+ use std::{mem, ptr};
4
4
 
5
5
  use swc_core::ecma::ast::Function;
6
6
 
7
7
  use super::copies::Copies;
8
- use super::run::{Run, Stop};
8
+ use super::intrinsic;
9
+ use super::run::{Decrypted, Run, Snapshot, Stop};
9
10
  use crate::vm::dispatch::Decoded;
10
11
  use crate::vm::output::Path;
11
12
  use crate::vm::session::Session;
@@ -26,6 +27,8 @@ pub(crate) struct Invocation<'session, 'ast> {
26
27
  pub handler: &'ast Function,
27
28
  /// The instance fields the constructor derives from the program.
28
29
  pub fields: &'session HashMap<String, Value<'ast>>,
30
+ /// Strings decrypted from the program so far.
31
+ pub decrypted: &'session Decrypted<'ast>,
29
32
  /// Registers whose values are known, such as a candidate string.
30
33
  pub known: &'session [(u8, Value<'ast>)],
31
34
  }
@@ -45,41 +48,147 @@ pub(crate) struct Way<'ast> {
45
48
 
46
49
  /// Runs the handler for each feasible combination of symbolic branches, depth-first with true
47
50
  /// before false. Returns `None` if there are more than [`MAXIMUM_PATHS`].
51
+ ///
52
+ /// Paths that differ only after the top-level statement where the handler's first branch was
53
+ /// seen resume from a [`Snapshot`] taken before it rather than rerunning the whole handler.
54
+ /// Each path still sees exactly the state a rerun would.
48
55
  #[must_use]
49
56
  pub(crate) fn explore<'ast>(invocation: &Invocation<'_, 'ast>) -> Option<Ways<'ast>> {
50
- let mut ways = Ways::default();
51
- let handler = Callable::Script {
52
- name: None,
53
- function: invocation.handler,
54
- scope: None,
57
+ let session = invocation.session;
58
+ let handler = invocation.handler;
59
+ let address = intrinsic::address(handler);
60
+ // `Run::script` runs these natively or caches them, rather than through a slot plan.
61
+ let special = session.intrinsics.contains_key(&address)
62
+ || session
63
+ .record_decoder
64
+ .is_some_and(|decoder| ptr::eq(decoder, handler));
65
+ let plan = if special {
66
+ None
67
+ } else {
68
+ session.compiled.slots(handler)
55
69
  };
56
- let mut decisions = Vec::new();
57
- for _ in 0..MAXIMUM_PATHS {
58
- let mut run = Run::handler(invocation, decisions);
59
- let outcome = run.invoke(&handler, Value::Undefined, vec![Value::Machine]);
60
- decisions = mem::take(&mut run.branches.decisions);
61
- decisions.truncate(run.branches.decided);
62
- let scopes = run.scopes();
63
- let fields = mem::take(&mut run.fields);
64
- let way = run.finish(outcome);
65
- // Clear captured scopes to break cycles between functions and the scopes holding them.
66
- for scope in scopes {
67
- scope.borrow_mut().variables.clear();
68
- }
69
- match way {
70
- Ok(path) => ways.taken.push(Way { path, fields }),
71
- Err(fault) => ways.faults.push(fault),
72
- }
73
- // Retry the last branch taken as `true` that has not yet been tried as `false`.
74
- while decisions.last() == Some(&false) {
75
- decisions.pop();
76
- }
77
- match decisions.last_mut() {
78
- Some(last) => *last = false,
79
- None => return Some(ways),
70
+ // A run that thaws a shared session object leaves no results; explore again with the
71
+ // object copied.
72
+ 'explore: loop {
73
+ let branching = session
74
+ .branching
75
+ .borrow()
76
+ .get(&address)
77
+ .copied()
78
+ .unwrap_or_default();
79
+ // Snapshots usually repay their copying only for handlers whose instructions have
80
+ // averaged at least 1.5 runs.
81
+ let split = branching
82
+ .statement
83
+ .filter(|&statement| statement > 0 && 2 * branching.runs >= 3 * branching.instructions);
84
+ let mut ways = Ways::default();
85
+ let mut decisions = Vec::new();
86
+ let mut snapshot: Option<Snapshot<'ast>> = None;
87
+ for _ in 0..MAXIMUM_PATHS {
88
+ let resumed = snapshot
89
+ .as_ref()
90
+ .filter(|snapshot| snapshot.resumes(&decisions));
91
+ let (mut run, outcome, copied) = match (plan.as_deref(), resumed) {
92
+ (Some(plan), Some(snapshot)) => {
93
+ let (mut run, mut locals, copied) =
94
+ Run::resume(invocation, snapshot, decisions);
95
+ // Snapshot locals are dropped rather than returned to the spare buffers.
96
+ let outcome = run.handler_statements(
97
+ plan,
98
+ snapshot.statement,
99
+ &mut locals,
100
+ &mut |_, _, _| {},
101
+ );
102
+ (run, outcome, copied)
103
+ }
104
+ (Some(plan), None) => {
105
+ let mut run = Run::handler(invocation, decisions);
106
+ let mut taken = None;
107
+ // The last top-level statement started before any decision was consumed.
108
+ let mut undecided = None;
109
+ let outcome = run.call_handler(
110
+ plan,
111
+ Value::Undefined,
112
+ vec![Value::Machine],
113
+ &mut |run, statement, locals| {
114
+ if run.branches.decided == 0 {
115
+ undecided = Some(statement);
116
+ }
117
+ if split == Some(statement) {
118
+ taken = run.snapshot(statement, locals);
119
+ }
120
+ },
121
+ );
122
+ if branching.statement.is_none()
123
+ && !run.thawed
124
+ && run.branches.decided > 0
125
+ && let Some(statement) = undecided
126
+ {
127
+ let mut learned = session.branching.borrow_mut();
128
+ learned
129
+ .entry(address)
130
+ .or_default()
131
+ .statement
132
+ .get_or_insert(statement);
133
+ }
134
+ snapshot = taken;
135
+ (run, outcome, Vec::new())
136
+ }
137
+ (None, _) => {
138
+ let mut run = Run::handler(invocation, decisions);
139
+ let handler = Callable::Script {
140
+ name: None,
141
+ function: handler,
142
+ scope: None,
143
+ };
144
+ let outcome = run.invoke(&handler, Value::Undefined, vec![Value::Machine]);
145
+ (run, outcome, Vec::new())
146
+ }
147
+ };
148
+ decisions = mem::take(&mut run.branches.decisions);
149
+ decisions.truncate(run.branches.decided);
150
+ let thawed = run.thawed;
151
+ let scopes = run.scopes();
152
+ let fields = mem::take(&mut run.fields);
153
+ let way = run.finish(outcome);
154
+ // Clear captured scopes to break cycles between functions and the scopes holding
155
+ // them.
156
+ for scope in scopes.into_iter().chain(copied) {
157
+ scope.borrow_mut().variables.clear();
158
+ }
159
+ if thawed {
160
+ continue 'explore;
161
+ }
162
+ match way {
163
+ Ok(path) => ways.taken.push(Way { path, fields }),
164
+ Err(fault) => ways.faults.push(fault),
165
+ }
166
+ // Retry the last branch taken as `true` that has not yet been tried as `false`.
167
+ while decisions.last() == Some(&false) {
168
+ decisions.pop();
169
+ }
170
+ let Some(last) = decisions.last_mut() else {
171
+ let mut learned = session.branching.borrow_mut();
172
+ let learned = learned.entry(address).or_default();
173
+ learned.instructions += 1;
174
+ learned.runs += ways.taken.len() + ways.faults.len();
175
+ return Some(ways);
176
+ };
177
+ *last = false;
80
178
  }
179
+ break None;
81
180
  }
82
- None
181
+ }
182
+
183
+ /// What exploration has learned about a handler's paths.
184
+ #[derive(Clone, Copy, Default)]
185
+ pub(crate) struct Branching {
186
+ /// The top-level statement where its first decision was seen.
187
+ statement: Option<usize>,
188
+ /// Instructions explored.
189
+ instructions: usize,
190
+ /// Runs the explored instructions took.
191
+ runs: usize,
83
192
  }
84
193
 
85
194
  /// Runs `decoder` after setting the program counter and dispatch key registers. Returns the decoded
@@ -92,18 +201,25 @@ pub(crate) fn decode<'ast>(
92
201
  pc: u32,
93
202
  key: u8,
94
203
  ) -> Result<Decoded, String> {
95
- let mut run = Run::new(session, program, Vec::new(), Some(Copies::default()));
96
- run.start_at(pc, key);
97
204
  let decoder = Callable::Script {
98
205
  name: None,
99
206
  function: decoder,
100
207
  scope: None,
101
208
  };
102
- let outcome = run.invoke(&decoder, Value::Undefined, arguments);
103
- let next = run.register_value(session.layout.key);
104
- for scope in run.scopes() {
105
- scope.borrow_mut().variables.clear();
106
- }
209
+ // A run that thaws a shared session object decodes again with the object copied.
210
+ let (outcome, next) = loop {
211
+ let mut run = Run::new(session, program, Vec::new(), Some(Copies::default()));
212
+ run.sharing = true;
213
+ run.start_at(pc, key);
214
+ let outcome = run.invoke(&decoder, Value::Undefined, arguments.clone());
215
+ let next = run.register_value(session.layout.key);
216
+ for scope in run.scopes() {
217
+ scope.borrow_mut().variables.clear();
218
+ }
219
+ if !run.thawed {
220
+ break (outcome, next);
221
+ }
222
+ };
107
223
  let opcode = outcome.map_err(|stop| match *stop {
108
224
  Stop::Throw(value) => format!("throws {}", value.expr()),
109
225
  Stop::Unsupported(reason) => reason,
@@ -0,0 +1,171 @@
1
+ //! Session objects that handler runs share instead of copying while no run changes them.
2
+ //!
3
+ //! Runs copy every session object they reach (see [`super::copies`]), but most session
4
+ //! bindings are tables that handlers only read. Objects reachable from bindings form groups:
5
+ //! bindings that share an object belong to one group. Handler and decoder runs share a
6
+ //! group's objects as they are. The first run that writes to one thaws its group, so later
7
+ //! runs copy it as before, and stops; the caller then reruns the instruction from the start.
8
+ //! Every completed run therefore sees what a run that copied everything would.
9
+ //!
10
+ //! Groups that reach a function with captured state are never shared: calling it would write
11
+ //! to a session scope.
12
+
13
+ use std::cell::RefCell;
14
+ use std::rc::Rc;
15
+
16
+ use rustc_hash::FxHashSet as HashSet;
17
+ use swc_core::atoms::Atom;
18
+
19
+ use crate::vm::HashMap;
20
+ use crate::vm::value::Value;
21
+
22
+ #[derive(Default)]
23
+ pub(crate) struct Frozen {
24
+ /// Addresses of the objects runs share.
25
+ shared: RefCell<HashSet<usize>>,
26
+ /// Each shared object's group root, by address.
27
+ groups: HashMap<usize, usize>,
28
+ /// Each group's objects, by root.
29
+ members: HashMap<usize, Vec<usize>>,
30
+ }
31
+
32
+ impl Frozen {
33
+ /// Groups the objects reachable from `bindings` and shares those of groups that reach no
34
+ /// function with captured state.
35
+ pub(crate) fn of(bindings: &HashMap<Atom, Value<'_>>) -> Self {
36
+ let mut groups = Groups::default();
37
+ for value in bindings.values() {
38
+ let mut reached = Reached::default();
39
+ reached.visit(value);
40
+ groups.join(&reached);
41
+ }
42
+ groups.frozen()
43
+ }
44
+
45
+ /// Whether runs share the object at `address`.
46
+ pub(super) fn contains(&self, address: usize) -> bool {
47
+ let shared = self.shared.borrow();
48
+ !shared.is_empty() && shared.contains(&address)
49
+ }
50
+
51
+ /// Stops sharing the group of the object at `address`.
52
+ pub(super) fn thaw(&self, address: usize) {
53
+ if let Some(&group) = self.groups.get(&address) {
54
+ let mut shared = self.shared.borrow_mut();
55
+ for member in &self.members[&group] {
56
+ shared.remove(member);
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ /// The address of `value` if it is an object a run can change.
63
+ pub(super) fn address(value: &Value<'_>) -> Option<usize> {
64
+ Some(match value {
65
+ Value::Array(elements) => Rc::as_ptr(elements).addr(),
66
+ Value::Object(properties) => Rc::as_ptr(properties).addr(),
67
+ Value::Set(members) => Rc::as_ptr(members).addr(),
68
+ Value::Map(entries) => Rc::as_ptr(entries).addr(),
69
+ _ => return None,
70
+ })
71
+ }
72
+
73
+ /// The objects reachable from one binding.
74
+ #[derive(Default)]
75
+ struct Reached {
76
+ seen: HashSet<usize>,
77
+ /// Whether a function with captured state is reachable.
78
+ stateful: bool,
79
+ }
80
+
81
+ impl Reached {
82
+ fn visit(&mut self, value: &Value<'_>) {
83
+ if let Value::Function(_) = value {
84
+ self.stateful |= !value.is_immutable();
85
+ return;
86
+ }
87
+ let Some(address) = address(value) else {
88
+ return;
89
+ };
90
+ if !self.seen.insert(address) {
91
+ return;
92
+ }
93
+ match value {
94
+ Value::Array(elements) | Value::Set(elements) => {
95
+ for element in elements.borrow().iter() {
96
+ self.visit(element);
97
+ }
98
+ }
99
+ Value::Object(properties) => {
100
+ for (_, property) in properties.borrow().iter() {
101
+ self.visit(property);
102
+ }
103
+ }
104
+ Value::Map(entries) => {
105
+ for (key, entry) in entries.borrow().iter() {
106
+ self.visit(key);
107
+ self.visit(entry);
108
+ }
109
+ }
110
+ _ => {}
111
+ }
112
+ }
113
+ }
114
+
115
+ /// Union-find over objects, joining the objects each binding reaches.
116
+ #[derive(Default)]
117
+ struct Groups {
118
+ /// Each object's parent in the union-find forest, by address.
119
+ parents: HashMap<usize, usize>,
120
+ /// Roots whose group reaches a function with captured state.
121
+ stateful: HashSet<usize>,
122
+ }
123
+
124
+ impl Groups {
125
+ fn root(&mut self, mut address: usize) -> usize {
126
+ self.parents.entry(address).or_insert(address);
127
+ while let Some(&parent) = self.parents.get(&address)
128
+ && parent != address
129
+ {
130
+ let grandparent = self.parents[&parent];
131
+ self.parents.insert(address, grandparent);
132
+ address = grandparent;
133
+ }
134
+ address
135
+ }
136
+
137
+ fn join(&mut self, reached: &Reached) {
138
+ let mut objects = reached.seen.iter().copied();
139
+ let Some(first) = objects.next() else {
140
+ return;
141
+ };
142
+ let root = self.root(first);
143
+ for object in objects {
144
+ let other = self.root(object);
145
+ if other != root {
146
+ self.parents.insert(other, root);
147
+ if self.stateful.remove(&other) {
148
+ self.stateful.insert(root);
149
+ }
150
+ }
151
+ }
152
+ if reached.stateful {
153
+ self.stateful.insert(root);
154
+ }
155
+ }
156
+
157
+ fn frozen(mut self) -> Frozen {
158
+ let objects: Vec<_> = self.parents.keys().copied().collect();
159
+ let mut frozen = Frozen::default();
160
+ for object in objects {
161
+ let root = self.root(object);
162
+ if self.stateful.contains(&root) {
163
+ continue;
164
+ }
165
+ frozen.members.entry(root).or_default().push(object);
166
+ frozen.groups.insert(object, root);
167
+ frozen.shared.get_mut().insert(object);
168
+ }
169
+ frozen
170
+ }
171
+ }