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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +10 -9
- data/UPSTREAM.md +6 -6
- data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
- data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
- data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
- data/lib/cfsolve/version.rb +2 -2
- data/vendor/cfsolve/README.md +0 -35
- data/vendor/cfsolve/examples/build_profile.rs +17 -15
- data/vendor/cfsolve/examples/capture_session.rs +37 -0
- data/vendor/cfsolve/examples/check_answers.rs +66 -0
- data/vendor/cfsolve/examples/compare_session.rs +190 -0
- data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
- data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
- data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
- data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
- data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
- data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
- data/vendor/cfsolve/src/capture/profile.rs +7 -18
- data/vendor/cfsolve/src/capture/recorded.rs +42 -7
- data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
- data/vendor/cfsolve/src/challenge/installed.rs +122 -271
- data/vendor/cfsolve/src/challenge/payload.rs +107 -5
- data/vendor/cfsolve/src/challenge/state.rs +98 -16
- data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
- data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
- data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
- data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
- data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
- data/vendor/cfsolve/src/http/client.rs +11 -0
- data/vendor/cfsolve/src/js/ast.rs +12 -3
- data/vendor/cfsolve/src/js/objects.rs +9 -2
- data/vendor/cfsolve/src/listing/fold.rs +0 -83
- data/vendor/cfsolve/src/listing/query.rs +25 -244
- data/vendor/cfsolve/src/listing.rs +0 -1
- data/vendor/cfsolve/src/session/initial.rs +14 -5
- data/vendor/cfsolve/src/session/probes.rs +160 -125
- data/vendor/cfsolve/src/session/timing.rs +58 -53
- data/vendor/cfsolve/src/session/widget.rs +5 -6
- data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
- data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
- data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
- data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
- data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
- data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
- data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
- data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
- data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
- data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
- data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
- data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
- data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
- data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
- data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
- data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
- data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
- data/vendor/cfsolve/src/stage2/probes.rs +0 -1
- data/vendor/cfsolve/src/stage2/records.rs +4 -13
- data/vendor/cfsolve/src/stage2/reference.json +1 -1
- data/vendor/cfsolve/src/stage2/reference.rs +98 -100
- data/vendor/cfsolve/src/stage2/registry.rs +41 -34
- data/vendor/cfsolve/src/stage2/replay.rs +182 -254
- data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
- data/vendor/cfsolve/src/stage2/trace.rs +18 -0
- data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
- data/vendor/cfsolve/src/stage2.rs +2 -1
- data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
- data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
- data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
- data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
- data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
- data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
- data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
- data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
- data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
- data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
- data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
- data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
- data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
- data/vendor/cfsolve/src/vm/interpret.rs +7 -2
- data/vendor/cfsolve/src/vm/machine.rs +6 -2
- data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
- data/vendor/cfsolve/src/vm/session.rs +36 -14
- data/vendor/cfsolve/src/vm/value.rs +13 -0
- data/vendor/cfsolve/src/vm.rs +1 -0
- data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
- metadata +13 -6
- data/vendor/cfsolve/src/listing/normalize.rs +0 -264
- data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
- data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
- data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
- data/vendor/cfsolve/src/stage2/session_names.rs +0 -470
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
//! Pairs iframe globals by their definitions, independently of randomized identifiers.
|
|
2
|
+
|
|
3
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
4
|
+
|
|
5
|
+
use anyhow::Result;
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
use swc_core::ecma::ast::{AssignExpr, AssignTarget, Expr, Lit, MemberExpr, SimpleAssignTarget};
|
|
8
|
+
use swc_core::ecma::visit::{Visit, VisitWith};
|
|
9
|
+
use url::Url;
|
|
10
|
+
|
|
11
|
+
use super::vocabulary;
|
|
12
|
+
use crate::challenge::installed::Installed;
|
|
13
|
+
use crate::challenge::program::PreparedSession;
|
|
14
|
+
use crate::js::ast;
|
|
15
|
+
use crate::script::Source;
|
|
16
|
+
|
|
17
|
+
/// A bag of syntax and browser API features. Local variable names, statement order, numeric
|
|
18
|
+
/// constants, and session names do not participate in matching.
|
|
19
|
+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
20
|
+
pub struct Signature {
|
|
21
|
+
kind: String,
|
|
22
|
+
features: BTreeMap<String, usize>,
|
|
23
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
24
|
+
value: Option<String>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Signatures of the iframe's session-named globals, by name.
|
|
28
|
+
#[derive(Default)]
|
|
29
|
+
pub struct Globals(pub BTreeMap<String, Signature>);
|
|
30
|
+
|
|
31
|
+
impl Globals {
|
|
32
|
+
/// Signatures of the hex-named globals the initial VM program installs.
|
|
33
|
+
pub(crate) fn installed(installed: &Installed) -> Self {
|
|
34
|
+
let mut globals = Self::default();
|
|
35
|
+
for (name, value) in &installed.audit {
|
|
36
|
+
if !vocabulary::is_hex_name(name) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let role = installed.audit_roles.get(name).map(String::as_str);
|
|
40
|
+
let feature = match role {
|
|
41
|
+
Some(role) => format!("role:{role}"),
|
|
42
|
+
None if vocabulary::is_hex_name(value) => "session-name".into(),
|
|
43
|
+
None => format!("value:{value}"),
|
|
44
|
+
};
|
|
45
|
+
let signature = Signature {
|
|
46
|
+
kind: "installed".into(),
|
|
47
|
+
features: BTreeMap::from([(feature, 1)]),
|
|
48
|
+
// Native function strings use each engine's whitespace and formatting.
|
|
49
|
+
// Preserve the captured classification instead of imposing Chromium's.
|
|
50
|
+
value: (!matches!(role, Some("clock-delta" | "native-array")))
|
|
51
|
+
.then(|| value.clone()),
|
|
52
|
+
};
|
|
53
|
+
globals.0.insert(name.clone(), signature);
|
|
54
|
+
}
|
|
55
|
+
globals
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Recovers both script definitions and globals installed by the initial VM program.
|
|
59
|
+
pub fn capture(source: &Source) -> Result<Self> {
|
|
60
|
+
let prepared =
|
|
61
|
+
PreparedSession::try_new(source, &Url::parse("https://challenges.cloudflare.com/")?)?;
|
|
62
|
+
let mut globals = Self::installed(&prepared.program().installed);
|
|
63
|
+
globals.0.extend(Self::recover(source).0);
|
|
64
|
+
Ok(globals)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Signatures of the hex-named globals the iframe script assigns on `window` or an alias.
|
|
68
|
+
pub(crate) fn recover(source: &Source) -> Self {
|
|
69
|
+
let mut aliases = Aliases(BTreeSet::from(["window".to_owned()]));
|
|
70
|
+
source.parsed().script.visit_with(&mut aliases);
|
|
71
|
+
let mut definitions = Definitions {
|
|
72
|
+
aliases: aliases.0,
|
|
73
|
+
globals: Self::default(),
|
|
74
|
+
};
|
|
75
|
+
source.parsed().script.visit_with(&mut definitions);
|
|
76
|
+
definitions.globals
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Accepts only a unique best candidate in both directions. Ambiguous definitions are
|
|
80
|
+
/// omitted rather than assigning an unrelated global the recorded value.
|
|
81
|
+
pub fn mapping(&self, recorded: &BTreeMap<String, Signature>) -> BTreeMap<String, String> {
|
|
82
|
+
let mut scores = Vec::new();
|
|
83
|
+
for (old, signature) in recorded {
|
|
84
|
+
for (new, candidate) in &self.0 {
|
|
85
|
+
let score = signature.similarity(candidate);
|
|
86
|
+
if score >= 0.9 {
|
|
87
|
+
scores.push((old, new, score));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
scores
|
|
92
|
+
.iter()
|
|
93
|
+
.filter(|(old, new, score)| {
|
|
94
|
+
!scores.iter().any(|(other_old, other_new, other_score)| {
|
|
95
|
+
(other_old == old && other_new != new || other_new == new && other_old != old)
|
|
96
|
+
&& other_score >= score
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
.map(|(old, new, _)| ((*old).clone(), (*new).clone()))
|
|
100
|
+
.collect()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Signature {
|
|
105
|
+
/// A global's initial audit value when recovered from VM initialization.
|
|
106
|
+
#[must_use]
|
|
107
|
+
pub fn audit_value(&self) -> Option<&str> {
|
|
108
|
+
self.value.as_deref()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// The startup timer's measured delta varies between otherwise equivalent sessions.
|
|
112
|
+
#[must_use]
|
|
113
|
+
pub fn is_elapsed_time(&self) -> bool {
|
|
114
|
+
self.features.contains_key("role:clock-delta")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn similarity(&self, other: &Self) -> f64 {
|
|
118
|
+
if self.kind != other.kind {
|
|
119
|
+
return 0.0;
|
|
120
|
+
}
|
|
121
|
+
let mut intersection = 0;
|
|
122
|
+
let mut union = 0;
|
|
123
|
+
for key in self
|
|
124
|
+
.features
|
|
125
|
+
.keys()
|
|
126
|
+
.chain(other.features.keys())
|
|
127
|
+
.collect::<BTreeSet<_>>()
|
|
128
|
+
{
|
|
129
|
+
let a = self.features.get(key).copied().unwrap_or_default();
|
|
130
|
+
let b = other.features.get(key).copied().unwrap_or_default();
|
|
131
|
+
intersection += a.min(b);
|
|
132
|
+
union += a.max(b);
|
|
133
|
+
}
|
|
134
|
+
let structural = intersection as f64 / union.max(1) as f64;
|
|
135
|
+
let anchors = |signature: &Self| {
|
|
136
|
+
signature
|
|
137
|
+
.features
|
|
138
|
+
.keys()
|
|
139
|
+
.filter(|key| key.starts_with("property:") || key.starts_with("string:"))
|
|
140
|
+
.cloned()
|
|
141
|
+
.collect::<BTreeSet<_>>()
|
|
142
|
+
};
|
|
143
|
+
let own_anchors = anchors(self);
|
|
144
|
+
if self.kind == "function" && own_anchors.len() >= 2 && own_anchors == anchors(other) {
|
|
145
|
+
// Deobfuscation can duplicate reads and move local declarations while preserving
|
|
146
|
+
// the APIs and fixed strings that identify the helper.
|
|
147
|
+
structural.max(0.9 + 0.1 * structural)
|
|
148
|
+
} else {
|
|
149
|
+
structural
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn feature(&mut self, feature: String) {
|
|
154
|
+
*self.features.entry(feature).or_default() += 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
struct Aliases(BTreeSet<String>);
|
|
159
|
+
|
|
160
|
+
impl Visit for Aliases {
|
|
161
|
+
fn visit_member_expr(&mut self, member: &MemberExpr) {
|
|
162
|
+
if ast::member_str(&member.prop) == Some("_cf_chl_opt")
|
|
163
|
+
&& let Expr::Ident(alias) = member.obj.unwrap_parens()
|
|
164
|
+
{
|
|
165
|
+
self.0.insert(alias.sym.to_string());
|
|
166
|
+
}
|
|
167
|
+
member.visit_children_with(self);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
struct Definitions {
|
|
172
|
+
aliases: BTreeSet<String>,
|
|
173
|
+
globals: Globals,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
impl Visit for Definitions {
|
|
177
|
+
fn visit_assign_expr(&mut self, assignment: &AssignExpr) {
|
|
178
|
+
if let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = &assignment.left
|
|
179
|
+
&& let Expr::Ident(alias) = member.obj.unwrap_parens()
|
|
180
|
+
&& self.aliases.contains(alias.sym.as_ref())
|
|
181
|
+
&& let Some(name) = ast::member_str(&member.prop)
|
|
182
|
+
&& vocabulary::is_hex_name(name)
|
|
183
|
+
{
|
|
184
|
+
let signature = self.globals.0.entry(name.to_owned()).or_default();
|
|
185
|
+
if signature.kind.is_empty() {
|
|
186
|
+
signature.kind = match assignment.right.unwrap_parens() {
|
|
187
|
+
Expr::Fn(_) | Expr::Arrow(_) => "function",
|
|
188
|
+
Expr::Object(_) => "object",
|
|
189
|
+
Expr::Array(_) => "array",
|
|
190
|
+
_ => "value",
|
|
191
|
+
}
|
|
192
|
+
.into();
|
|
193
|
+
}
|
|
194
|
+
assignment.right.visit_with(signature);
|
|
195
|
+
}
|
|
196
|
+
assignment.visit_children_with(self);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
impl Visit for Signature {
|
|
201
|
+
fn visit_expr(&mut self, expression: &Expr) {
|
|
202
|
+
let feature = match expression {
|
|
203
|
+
Expr::Bin(value) => format!("binary:{:?}", value.op),
|
|
204
|
+
Expr::Unary(value) => format!("unary:{:?}", value.op),
|
|
205
|
+
Expr::Call(_) => "call".into(),
|
|
206
|
+
Expr::New(_) => "new".into(),
|
|
207
|
+
Expr::Cond(_) => "conditional".into(),
|
|
208
|
+
Expr::Fn(_) | Expr::Arrow(_) => "function".into(),
|
|
209
|
+
Expr::Array(_) => "array".into(),
|
|
210
|
+
Expr::Object(_) => "object".into(),
|
|
211
|
+
Expr::Assign(value) => format!("assign:{:?}", value.op),
|
|
212
|
+
Expr::Lit(Lit::Bool(value)) => format!("bool:{}", value.value),
|
|
213
|
+
_ => "expression".into(),
|
|
214
|
+
};
|
|
215
|
+
self.feature(feature);
|
|
216
|
+
if let Some(text) = ast::static_str(expression)
|
|
217
|
+
&& !vocabulary::is_hex_name(text)
|
|
218
|
+
&& text.len() <= 64
|
|
219
|
+
&& text
|
|
220
|
+
.chars()
|
|
221
|
+
.all(|character| character.is_ascii_alphabetic() || " ._-/".contains(character))
|
|
222
|
+
{
|
|
223
|
+
self.feature(format!("string:{text}"));
|
|
224
|
+
}
|
|
225
|
+
expression.visit_children_with(self);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn visit_member_expr(&mut self, member: &MemberExpr) {
|
|
229
|
+
let randomized = |name: &str| {
|
|
230
|
+
vocabulary::is_hex_name(name)
|
|
231
|
+
|| name.strip_prefix("_0x").is_some_and(|suffix| {
|
|
232
|
+
!suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_hexdigit())
|
|
233
|
+
})
|
|
234
|
+
};
|
|
235
|
+
if let Some(name) = ast::member_str(&member.prop)
|
|
236
|
+
&& !randomized(name)
|
|
237
|
+
{
|
|
238
|
+
self.feature(format!("property:{name}"));
|
|
239
|
+
}
|
|
240
|
+
member.visit_children_with(self);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[cfg(test)]
|
|
245
|
+
mod tests {
|
|
246
|
+
use super::*;
|
|
247
|
+
|
|
248
|
+
fn globals(script: &str) -> Globals {
|
|
249
|
+
Globals::recover(&Source::try_new(script).unwrap())
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn pairs_definitions_across_aliases_and_local_names() {
|
|
254
|
+
let old = globals(
|
|
255
|
+
r#"
|
|
256
|
+
var x = window; x._cf_chl_opt;
|
|
257
|
+
window._111111111111111111111111 = function(a) { return a.createHTML(); };
|
|
258
|
+
x._222222222222222222222222 = function(b) { return b.createScript(); };
|
|
259
|
+
"#,
|
|
260
|
+
);
|
|
261
|
+
let new = globals(
|
|
262
|
+
r#"
|
|
263
|
+
var y = window; y._cf_chl_opt;
|
|
264
|
+
y._aaaaaaaaaaaaaaaaaaaaaaaa = function(c) { return c.createScript(); };
|
|
265
|
+
window._bbbbbbbbbbbbbbbbbbbbbbbb = function(d) { return d.createHTML(); };
|
|
266
|
+
"#,
|
|
267
|
+
);
|
|
268
|
+
let mapping = new.mapping(&old.0);
|
|
269
|
+
assert_eq!(mapping.len(), 2);
|
|
270
|
+
assert_eq!(
|
|
271
|
+
mapping["_111111111111111111111111"],
|
|
272
|
+
"_bbbbbbbbbbbbbbbbbbbbbbbb"
|
|
273
|
+
);
|
|
274
|
+
assert_eq!(
|
|
275
|
+
mapping["_222222222222222222222222"],
|
|
276
|
+
"_aaaaaaaaaaaaaaaaaaaaaaaa"
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[test]
|
|
281
|
+
fn ambiguous_or_different_definitions_are_omitted() {
|
|
282
|
+
let old = globals("window._111111111111111111111111 = function(a) { return a; };");
|
|
283
|
+
let duplicates = globals(
|
|
284
|
+
r#"
|
|
285
|
+
window._aaaaaaaaaaaaaaaaaaaaaaaa = function(b) { return b; };
|
|
286
|
+
window._bbbbbbbbbbbbbbbbbbbbbbbb = function(c) { return c; };
|
|
287
|
+
"#,
|
|
288
|
+
);
|
|
289
|
+
assert!(duplicates.mapping(&old.0).is_empty());
|
|
290
|
+
let different = globals("window._aaaaaaaaaaaaaaaaaaaaaaaa = { value: true };");
|
|
291
|
+
assert!(different.mapping(&old.0).is_empty());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[test]
|
|
295
|
+
fn injected_operator_helpers_do_not_change_api_identity() {
|
|
296
|
+
let old = globals(
|
|
297
|
+
"window._111111111111111111111111 = \
|
|
298
|
+
function(a) { return a.getOwnPropertyNames().concat(a.keys()); };",
|
|
299
|
+
);
|
|
300
|
+
let new = globals(
|
|
301
|
+
"window._aaaaaaaaaaaaaaaaaaaaaaaa = function(b) { \
|
|
302
|
+
var h = {_0xabc: function(x) { return x; }}; h._0xabc(b); \
|
|
303
|
+
return b.getOwnPropertyNames().concat(b.keys()); };",
|
|
304
|
+
);
|
|
305
|
+
assert_eq!(
|
|
306
|
+
new.mapping(&old.0)["_111111111111111111111111"],
|
|
307
|
+
"_aaaaaaaaaaaaaaaaaaaaaaaa"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -163,15 +163,11 @@ pub(crate) fn recognize(probe: &ProbeView<'_>) -> Option<Fields> {
|
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
|
|
166
|
+
// Programs that decode strings in place also hold the source as a descriptor.
|
|
167
|
+
let parsed = strings
|
|
167
168
|
.iter()
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
.or_else(|_| {
|
|
171
|
-
let decoded = listing::decode_string(source, "")?;
|
|
172
|
-
ast::parse_javascript("benchmark worker", &decoded)
|
|
173
|
-
})
|
|
174
|
-
.ok()?;
|
|
169
|
+
.filter(|text| text.contains("WebAssembly.instantiate"))
|
|
170
|
+
.find_map(|source| ast::parse_javascript("benchmark worker", source).ok())?;
|
|
175
171
|
let mut worker = Worker::default();
|
|
176
172
|
parsed.script.visit_with(&mut worker);
|
|
177
173
|
// Only the kernels the recorded speeds measure are supported. Field names are recovered
|
|
@@ -204,6 +200,7 @@ pub(crate) fn recognize(probe: &ProbeView<'_>) -> Option<Fields> {
|
|
|
204
200
|
.find(|items| items.len() == order.len() && *items != order)?;
|
|
205
201
|
// The worker's source begins `var plan={<order>:[...],<iterations>:[...],`,
|
|
206
202
|
// and the record takes the same two names.
|
|
203
|
+
// Hex names start with `_`.
|
|
207
204
|
let identifier = |text: &str| {
|
|
208
205
|
!text.is_empty()
|
|
209
206
|
&& text
|
|
@@ -153,14 +153,23 @@ pub struct MethodCall {
|
|
|
153
153
|
pub arity: usize,
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/// Call path to `new Error()` in the stack probe. The
|
|
157
|
-
///
|
|
156
|
+
/// Call path to `new Error()` in the stack probe. The probe function calls a closure that
|
|
157
|
+
/// constructs the error.
|
|
158
158
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
|
159
159
|
pub struct ProbeCalls {
|
|
160
160
|
/// The opcode that constructs `new Error()`.
|
|
161
161
|
pub construct: u8,
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// The callback that last resumed the VM before the stack probe, whose listener is the probe's
|
|
165
|
+
/// outermost frame. Chrome and Safari captures show both; a Worker message is more common.
|
|
166
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
167
|
+
pub enum Resumption {
|
|
168
|
+
/// A timer, whose callback receives no arguments. V8 leaves its listener anonymous.
|
|
169
|
+
Timer,
|
|
170
|
+
/// A Worker's message event, which passes the event. V8 names the listener by its
|
|
171
|
+
/// receiver, `Worker.<anonymous>`.
|
|
172
|
+
WorkerMessage,
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
/// Recovers callback frames for a VM function with `arity` parameters and error-construction
|
|
@@ -179,15 +188,24 @@ pub fn recover(source: &str, cases: &CaseOpcodes, construct: u8, arity: usize) -
|
|
|
179
188
|
})
|
|
180
189
|
}
|
|
181
190
|
|
|
182
|
-
/// Recovers frames for the stack probe
|
|
183
|
-
/// JavaScript calls, so frames stop at the listener that
|
|
184
|
-
/// innermost ten frames.
|
|
185
|
-
pub fn recover_probe(
|
|
191
|
+
/// Recovers frames for the stack probe, run after `resumed`. The VM executes closures in its
|
|
192
|
+
/// dispatch loop without JavaScript calls, so frames stop at the listener that resumed it. V8
|
|
193
|
+
/// retains only the innermost ten frames.
|
|
194
|
+
pub fn recover_probe(
|
|
195
|
+
source: &str,
|
|
196
|
+
cases: &CaseOpcodes,
|
|
197
|
+
calls: ProbeCalls,
|
|
198
|
+
resumed: Resumption,
|
|
199
|
+
) -> Result<Geometry> {
|
|
186
200
|
geometry(source, cases, |machine| {
|
|
187
201
|
// The probe registers an exception handler, so its instructions dispatch from the
|
|
188
202
|
// guarded loop.
|
|
189
203
|
let mut chain = machine.construction(calls.construct, true)?;
|
|
190
|
-
|
|
204
|
+
let (arity, name) = match resumed {
|
|
205
|
+
Resumption::Timer => (0, ""),
|
|
206
|
+
Resumption::WorkerMessage => (1, "Worker.<anonymous>"),
|
|
207
|
+
};
|
|
208
|
+
chain.frames.extend(machine.listener(arity, name)?);
|
|
191
209
|
chain.frames.truncate(V8_STACK_LIMIT);
|
|
192
210
|
Ok(chain)
|
|
193
211
|
})
|
|
@@ -648,6 +666,8 @@ struct Script {
|
|
|
648
666
|
/// Array slots containing `Reflect.construct`, indexed by array and slot.
|
|
649
667
|
construct_slots: Vec<(Id, usize)>,
|
|
650
668
|
applies: Vec<Id>,
|
|
669
|
+
/// Array slots containing `Reflect.apply`, indexed by array and slot.
|
|
670
|
+
apply_slots: Vec<(Id, usize)>,
|
|
651
671
|
appliers: Appliers,
|
|
652
672
|
}
|
|
653
673
|
|
|
@@ -816,6 +836,7 @@ impl Script {
|
|
|
816
836
|
constructs: Vec::new(),
|
|
817
837
|
construct_slots: Vec::new(),
|
|
818
838
|
applies: Vec::new(),
|
|
839
|
+
apply_slots: Vec::new(),
|
|
819
840
|
appliers,
|
|
820
841
|
};
|
|
821
842
|
program.visit_with(&mut script);
|
|
@@ -858,9 +879,13 @@ impl Script {
|
|
|
858
879
|
.target(call)
|
|
859
880
|
.ok()?;
|
|
860
881
|
// Compare bindings so a nested function with the same name as a native apply
|
|
861
|
-
// alias cannot be mistaken for that alias.
|
|
862
|
-
let
|
|
863
|
-
|
|
882
|
+
// alias cannot be mistaken for that alias. Some builds keep the alias in an array slot.
|
|
883
|
+
let native = match (&target.callee, &target.slot) {
|
|
884
|
+
(Some(callee), _) => self.function(callee).is_err() || self.applies.contains(callee),
|
|
885
|
+
(None, Some(slot)) => self.apply_slots.contains(slot),
|
|
886
|
+
(None, None) => false,
|
|
887
|
+
};
|
|
888
|
+
if !native {
|
|
864
889
|
return None;
|
|
865
890
|
}
|
|
866
891
|
let [first, second, ..] = handed(call, &target)?[..] else {
|
|
@@ -888,13 +913,16 @@ impl Visit for Script {
|
|
|
888
913
|
(Some(name), Some("apply")) => self.applies.push(name.to_id()),
|
|
889
914
|
_ => {}
|
|
890
915
|
}
|
|
891
|
-
if
|
|
892
|
-
&& let AssignTarget::Simple(SimpleAssignTarget::Member(slot)) = &assign.left
|
|
916
|
+
if let AssignTarget::Simple(SimpleAssignTarget::Member(slot)) = &assign.left
|
|
893
917
|
&& let Some(array) = slot.obj.as_ident()
|
|
894
918
|
&& let Some(index) = slot.prop.as_computed()
|
|
895
919
|
&& let Some(index) = self.appliers.number(&index.expr).and_then(ast::array_index)
|
|
896
920
|
{
|
|
897
|
-
|
|
921
|
+
match method {
|
|
922
|
+
Some("construct") => self.construct_slots.push((array.to_id(), index)),
|
|
923
|
+
Some("apply") => self.apply_slots.push((array.to_id(), index)),
|
|
924
|
+
_ => {}
|
|
925
|
+
}
|
|
898
926
|
}
|
|
899
927
|
}
|
|
900
928
|
assign.visit_children_with(self);
|
|
@@ -1355,7 +1383,11 @@ impl<'script> Scope<'script> {
|
|
|
1355
1383
|
let constructor = name.starts_with(char::is_uppercase);
|
|
1356
1384
|
let mut scope = Self {
|
|
1357
1385
|
appliers,
|
|
1358
|
-
prefix: constructor
|
|
1386
|
+
prefix: if constructor {
|
|
1387
|
+
format!("{name}.")
|
|
1388
|
+
} else {
|
|
1389
|
+
String::new()
|
|
1390
|
+
},
|
|
1359
1391
|
forwarders: Forwarders::default(),
|
|
1360
1392
|
assigned: HashMap::new(),
|
|
1361
1393
|
aliases: HashMap::new(),
|
|
@@ -1857,6 +1889,30 @@ mod tests {
|
|
|
1857
1889
|
assert!(script.method_forwarder(&binding(&script, "S")).is_none());
|
|
1858
1890
|
}
|
|
1859
1891
|
|
|
1892
|
+
/// Checks method forwarders that call a `Reflect.apply` alias kept in an array slot, directly
|
|
1893
|
+
/// or through a selector helper. Other slots are not apply aliases.
|
|
1894
|
+
#[test]
|
|
1895
|
+
fn method_forwarders_may_apply_through_an_array_slot() {
|
|
1896
|
+
let source = "var G=[];G[2]=Reflect.apply;G[3]=function(){};\
|
|
1897
|
+
function W(s,f,a,b,c){switch(s^1){case 0:return f(a,b,c)}}\
|
|
1898
|
+
function D(f,r,a){return(0,G[2])(f,r,[a])}\
|
|
1899
|
+
function H(f,r,a,b){return W(1,G[2],f,r,[a,b])}\
|
|
1900
|
+
function O(f,r,a){return(0,G[3])(f,r,[a])}";
|
|
1901
|
+
let (script, _) = script(source);
|
|
1902
|
+
let cases: [(&str, &[&str]); 2] = [("D", &["D"]), ("H", &["W", "H"])];
|
|
1903
|
+
for (forwarder, expected) in cases {
|
|
1904
|
+
let frames = script
|
|
1905
|
+
.method_forwarder(&binding(&script, forwarder))
|
|
1906
|
+
.expect("`G[2]` holds `Reflect.apply`, so the call is a method forwarder");
|
|
1907
|
+
let names: Vec<&str> = frames.iter().map(|call| call.name.v8.as_str()).collect();
|
|
1908
|
+
assert_eq!(names, expected, "frames of forwarder {forwarder}");
|
|
1909
|
+
}
|
|
1910
|
+
assert!(
|
|
1911
|
+
script.method_forwarder(&binding(&script, "O")).is_none(),
|
|
1912
|
+
"`G[3]` holds a plain function, not `Reflect.apply`"
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1860
1916
|
#[test]
|
|
1861
1917
|
fn same_named_functions_resolve_to_the_called_binding() {
|
|
1862
1918
|
let (script, _) = script(
|
|
@@ -140,7 +140,7 @@ pub(crate) fn measure(
|
|
|
140
140
|
"bubbles":true, "button":0, "buttons":0, "cancelable":true,
|
|
141
141
|
"clientX":client_x, "clientY":client_y, "composed":true, "ctrlKey":false,
|
|
142
142
|
"defaultPrevented":false, "detail":1, "eventPhase":2, "height":1,
|
|
143
|
-
"isPrimary":is_primary, "isTrusted":true, "layerX":(client_x-x).
|
|
143
|
+
"isPrimary":is_primary, "isTrusted":true, "layerX":(client_x-x).floor(), "layerY":(client_y-y).floor(),
|
|
144
144
|
}));
|
|
145
145
|
event.extend(object(json!({
|
|
146
146
|
"movementX":0, "movementY":0, "metaKey":false,
|
|
@@ -250,6 +250,7 @@ mod tests {
|
|
|
250
250
|
assert_eq!(event["screenX"], 101.0);
|
|
251
251
|
assert_eq!(event["screenY"], 145.0);
|
|
252
252
|
assert_eq!(event["offsetY"], 12.0);
|
|
253
|
+
assert_eq!(event["layerY"], 11.0);
|
|
253
254
|
assert_eq!(raw["sources"]["derived"]["relative_y"], 11.0);
|
|
254
255
|
assert_eq!(event["timeStamp"], 123.0);
|
|
255
256
|
assert_eq!(raw["sources"]["element"]["clientWidth"], 168.0);
|
|
@@ -98,9 +98,6 @@ impl Plan {
|
|
|
98
98
|
|
|
99
99
|
/// Analyzes a returned checkbox program's listing.
|
|
100
100
|
pub fn recover(listing: &Listing) -> Result<Plan> {
|
|
101
|
-
let mut normalized = listing.clone();
|
|
102
|
-
crate::listing::normalize::constants(&mut normalized);
|
|
103
|
-
let listing = &normalized;
|
|
104
101
|
ensure!(
|
|
105
102
|
listing.failures.is_empty(),
|
|
106
103
|
"checkbox program has undecoded instructions"
|
|
@@ -180,7 +180,8 @@ pub(in crate::stage2) struct Recovered<'listing> {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/// Recovers the payload and any checkbox record. Tries unique header names as record-counter
|
|
183
|
-
/// candidates
|
|
183
|
+
/// candidates, then, for programs whose header holds no names, the fields published objects
|
|
184
|
+
/// start at zero. Uses `program` to identify errors.
|
|
184
185
|
pub(in crate::stage2) fn recover_unique<'listing>(
|
|
185
186
|
index: &Index<'listing>,
|
|
186
187
|
program: &str,
|
|
@@ -191,23 +192,18 @@ pub(in crate::stage2) fn recover_unique<'listing>(
|
|
|
191
192
|
let scan = scan(index, pool);
|
|
192
193
|
let mut layouts = Vec::new();
|
|
193
194
|
let mut failure = None;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
);
|
|
207
|
-
for counter in counters {
|
|
208
|
-
match scan.layout(counter) {
|
|
209
|
-
Ok(layout) => layouts.push((counter.clone(), layout)),
|
|
210
|
-
Err(error) => failure = Some(error),
|
|
195
|
+
for candidates in [
|
|
196
|
+
index.header().values().cloned().collect(),
|
|
197
|
+
scan.zeroed_fields(),
|
|
198
|
+
] {
|
|
199
|
+
for counter in candidates {
|
|
200
|
+
match scan.layout(&counter) {
|
|
201
|
+
Ok(layout) => layouts.push((counter, layout)),
|
|
202
|
+
Err(error) => failure = Some(error),
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if !layouts.is_empty() {
|
|
206
|
+
break;
|
|
211
207
|
}
|
|
212
208
|
}
|
|
213
209
|
ensure!(layouts.len() <= 1, "ambiguous {program} payload counter");
|
|
@@ -243,6 +239,35 @@ pub(in crate::stage2) struct RecordLiteral {
|
|
|
243
239
|
pub(in crate::stage2) struct Scan(Vec<FunctionWrites>);
|
|
244
240
|
|
|
245
241
|
impl Scan {
|
|
242
|
+
/// Fields a published object starts at zero that the program also writes to the payload as
|
|
243
|
+
/// a computed value, as it does when it increments the record counter. Other zeroed fields,
|
|
244
|
+
/// such as the probe count, are published as literals.
|
|
245
|
+
fn zeroed_fields(&self) -> Vec<String> {
|
|
246
|
+
let incremented = |field: &str| {
|
|
247
|
+
let mut payload_writes = self.0.iter().flat_map(FunctionWrites::payload_writes);
|
|
248
|
+
payload_writes.any(|write| {
|
|
249
|
+
write.key == field
|
|
250
|
+
&& !matches!(write.value, Written::Expression(Expression::Literal { .. }))
|
|
251
|
+
})
|
|
252
|
+
};
|
|
253
|
+
let mut fields: Vec<String> = Vec::new();
|
|
254
|
+
for function in &self.0 {
|
|
255
|
+
let Some(publication) = function.published else {
|
|
256
|
+
continue;
|
|
257
|
+
};
|
|
258
|
+
for write in &function.writes {
|
|
259
|
+
if write.is_zero()
|
|
260
|
+
&& write.target == WriteTarget::Object(publication.object)
|
|
261
|
+
&& !fields.contains(&write.key)
|
|
262
|
+
&& incremented(&write.key)
|
|
263
|
+
{
|
|
264
|
+
fields.push(write.key.clone());
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
fields
|
|
269
|
+
}
|
|
270
|
+
|
|
246
271
|
/// Recovers field sources for the second payload. Identifies the replacement template as the
|
|
247
272
|
/// object published to the payload cell with its record `counter` initialized to zero.
|
|
248
273
|
pub(in crate::stage2) fn layout(&self, counter: &str) -> Result<Layout> {
|
|
@@ -256,11 +281,9 @@ impl Scan {
|
|
|
256
281
|
.filter(|write| write.target == WriteTarget::Object(publication.object))
|
|
257
282
|
.collect();
|
|
258
283
|
fields.len() > 20
|
|
259
|
-
&& fields
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if value.as_f64() == Some(0.0))
|
|
263
|
-
})
|
|
284
|
+
&& fields
|
|
285
|
+
.iter()
|
|
286
|
+
.any(|write| write.key == counter && write.is_zero())
|
|
264
287
|
};
|
|
265
288
|
let templates = functions
|
|
266
289
|
.iter()
|
|
@@ -453,6 +476,14 @@ struct Write {
|
|
|
453
476
|
value: Written,
|
|
454
477
|
}
|
|
455
478
|
|
|
479
|
+
impl Write {
|
|
480
|
+
/// Whether the write stores a literal zero, as for the record counter in the template.
|
|
481
|
+
fn is_zero(&self) -> bool {
|
|
482
|
+
matches!(&self.value, Written::Expression(Expression::Literal { value })
|
|
483
|
+
if value.as_f64() == Some(0.0))
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
456
487
|
#[derive(Clone, Copy, PartialEq)]
|
|
457
488
|
enum WriteTarget {
|
|
458
489
|
/// A new object, identified by where it was created.
|