Skip to content

Commit 1541fb2

Browse files
authored
jax_fingerprint: Address user arg slot exhaustion (#13082)
When multiple jax_fingerprint.so instances are loaded (e.g., one per fingerprinting method), each instance was reserving its own user arg slot. ATS has a limited number of slots (~4 per type), causing methods like JA3 and JA4 to fail silently when slots were exhausted. Solution: Share a single user arg slot per type (TS_USER_ARGS_VCONN or TS_USER_ARGS_TXN) across all jax_fingerprint instances. A ContextMap stores JAxContext instances keyed by method name.
1 parent ad2940c commit 1541fb2

6 files changed

Lines changed: 421 additions & 14 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/** @file
2+
*
3+
* Shared context map for jax_fingerprint plugin instances.
4+
*
5+
* When multiple jax_fingerprint.so instances are loaded (e.g., one per
6+
* fingerprinting method), they share a single user arg slot. This map
7+
* stores the JAxContext for each method, keyed by method name.
8+
*
9+
* @section license License
10+
*
11+
* Licensed to the Apache Software Foundation (ASF) under one
12+
* or more contributor license agreements. See the NOTICE file
13+
* distributed with this work for additional information
14+
* regarding copyright ownership. The ASF licenses this file
15+
* to you under the Apache License, Version 2.0 (the
16+
* "License"); you may not use this file except in compliance
17+
* with the License. You may obtain a copy of the License at
18+
*
19+
* http://www.apache.org/licenses/LICENSE-2.0
20+
*
21+
* Unless required by applicable law or agreed to in writing, software
22+
* distributed under the License is distributed on an "AS IS" BASIS,
23+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24+
* See the License for the specific language governing permissions and
25+
* limitations under the License.
26+
*/
27+
28+
#pragma once
29+
30+
#include "config.h"
31+
#include "context.h"
32+
33+
#include <string>
34+
#include <string_view>
35+
#include <unordered_map>
36+
#include <version>
37+
38+
/**
39+
* @brief Container holding JAxContext instances for multiple methods.
40+
*
41+
* ATS has a limited number of user arg slots (~4 per type). When loading
42+
* many jax_fingerprint instances, we share a single slot and store all
43+
* contexts in this map, keyed by method name.
44+
*/
45+
class ContextMap
46+
{
47+
public:
48+
~ContextMap()
49+
{
50+
for (auto &pair : m_contexts) {
51+
delete pair.second;
52+
}
53+
}
54+
55+
/**
56+
* @brief Store a context for a method.
57+
* @param[in] method_name The method name (e.g., "JA3", "JA4").
58+
* @param[in] ctx The context to store. Ownership is transferred.
59+
*/
60+
void
61+
set(std::string_view method_name, JAxContext *ctx)
62+
{
63+
auto it = find_context(method_name);
64+
if (it != m_contexts.end()) {
65+
delete it->second;
66+
it->second = ctx;
67+
} else {
68+
m_contexts.emplace(std::string{method_name}, ctx);
69+
}
70+
}
71+
72+
/**
73+
* @brief Retrieve a context for a method.
74+
* @param[in] method_name The method name.
75+
* @return The context, or nullptr if not found.
76+
*/
77+
JAxContext *
78+
get(std::string_view method_name)
79+
{
80+
auto it = find_context(method_name);
81+
return it != m_contexts.end() ? it->second : nullptr;
82+
}
83+
84+
/**
85+
* @brief Remove a context for a method.
86+
* @param[in] method_name The method name.
87+
*/
88+
void
89+
remove(std::string_view method_name)
90+
{
91+
auto it = find_context(method_name);
92+
if (it != m_contexts.end()) {
93+
delete it->second;
94+
m_contexts.erase(it);
95+
}
96+
}
97+
98+
/**
99+
* @brief Check if the map is empty.
100+
* @return True if no contexts are stored.
101+
*/
102+
bool
103+
empty() const
104+
{
105+
return m_contexts.empty();
106+
}
107+
108+
private:
109+
using ContextStorage = std::unordered_map<std::string, JAxContext *, StringHash, std::equal_to<>>;
110+
111+
/** Find context by method name with C++20 generic lookup fallback.
112+
*
113+
* C++20 generic unordered lookup allows finding with std::string_view in a
114+
* std::unordered_map<std::string, ...> without creating a temporary string.
115+
* For standard libraries without this feature, we fall back to constructing
116+
* a std::string for the lookup.
117+
*
118+
* @param[in] method_name The method name to look up.
119+
* @return Iterator to the found element, or end() if not found.
120+
*/
121+
ContextStorage::iterator
122+
find_context(std::string_view method_name)
123+
{
124+
#ifdef __cpp_lib_generic_unordered_lookup
125+
return m_contexts.find(method_name);
126+
#else
127+
return m_contexts.find(std::string{method_name});
128+
#endif
129+
}
130+
131+
/** const_iterator @overload */
132+
ContextStorage::const_iterator
133+
find_context(std::string_view method_name) const
134+
{
135+
#ifdef __cpp_lib_generic_unordered_lookup
136+
return m_contexts.find(method_name);
137+
#else
138+
return m_contexts.find(std::string{method_name});
139+
#endif
140+
}
141+
142+
ContextStorage m_contexts;
143+
};

plugins/experimental/jax_fingerprint/plugin.cc

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,7 @@ handle_http_txn_close(void *edata, PluginConfig &config)
277277
{
278278
TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
279279

280-
delete get_user_arg(txnp, config);
281-
set_user_arg(txnp, config, nullptr);
280+
cleanup_user_arg(txnp, config);
282281

283282
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
284283
return TS_SUCCESS;
@@ -289,8 +288,7 @@ handle_vconn_close(void *edata, PluginConfig &config)
289288
{
290289
TSVConn vconn = static_cast<TSVConn>(edata);
291290

292-
delete get_user_arg(vconn, config);
293-
set_user_arg(vconn, config, nullptr);
291+
cleanup_user_arg(vconn, config);
294292

295293
TSVConnReenable(vconn);
296294
return TS_SUCCESS;

plugins/experimental/jax_fingerprint/userarg.cc

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,101 @@
2222
#include "plugin.h"
2323
#include "config.h"
2424
#include "userarg.h"
25+
#include "context_map.h"
26+
27+
namespace
28+
{
29+
30+
char const *
31+
user_arg_type_name(TSUserArgType type)
32+
{
33+
switch (type) {
34+
case TS_USER_ARGS_VCONN:
35+
return "vconn";
36+
case TS_USER_ARGS_TXN:
37+
return "txn";
38+
default:
39+
return "unknown";
40+
}
41+
}
42+
43+
// Shared user arg indices for all jax_fingerprint instances. ATS has limited
44+
// slots (~4 per type), so we share one slot per type and use a ContextMap.
45+
int vconn_user_arg_index = -1;
46+
int txn_user_arg_index = -1;
47+
bool vconn_slot_reserved = false;
48+
bool txn_slot_reserved = false;
49+
50+
} // anonymous namespace
2551

2652
int
2753
reserve_user_arg(PluginConfig &config)
2854
{
29-
std::string name = PLUGIN_NAME;
30-
name += config.method.name;
31-
3255
TSUserArgType type;
56+
int *shared_index;
57+
bool *reserved_flag;
58+
3359
if (config.method.type == Method::Type::CONNECTION_BASED) {
34-
type = TS_USER_ARGS_VCONN;
60+
type = TS_USER_ARGS_VCONN;
61+
shared_index = &vconn_user_arg_index;
62+
reserved_flag = &vconn_slot_reserved;
3563
} else {
36-
type = TS_USER_ARGS_TXN;
64+
type = TS_USER_ARGS_TXN;
65+
shared_index = &txn_user_arg_index;
66+
reserved_flag = &txn_slot_reserved;
3767
}
38-
int ret = TSUserArgIndexReserve(type, name.c_str(), "used to pass JAx context between hooks", &config.user_arg_index);
39-
Dbg(dbg_ctl, "user_arg_name: %s, user_arg_index: %d", name.c_str(), config.user_arg_index);
40-
return ret;
68+
69+
// Only reserve the slot once per type; subsequent calls reuse it.
70+
if (!*reserved_flag) {
71+
int ret = TSUserArgIndexReserve(type, PLUGIN_NAME, "shared JAx context map", shared_index);
72+
if (ret == TS_SUCCESS) {
73+
*reserved_flag = true;
74+
Dbg(dbg_ctl, "Reserved shared user_arg slot: type=%s, index=%d", user_arg_type_name(type), *shared_index);
75+
} else {
76+
Dbg(dbg_ctl, "Failed to reserve shared user_arg slot: type=%s", user_arg_type_name(type));
77+
return ret;
78+
}
79+
}
80+
81+
config.user_arg_index = *shared_index;
82+
Dbg(dbg_ctl, "Using shared user_arg: type=%s, method=%.*s, index=%d", user_arg_type_name(type),
83+
static_cast<int>(config.method.name.size()), config.method.name.data(), config.user_arg_index);
84+
return TS_SUCCESS;
4185
}
4286

4387
void
4488
set_user_arg(void *container, PluginConfig &config, JAxContext *ctx)
4589
{
46-
TSUserArgSet(container, config.user_arg_index, static_cast<void *>(ctx));
90+
ContextMap *map = static_cast<ContextMap *>(TSUserArgGet(container, config.user_arg_index));
91+
if (map == nullptr) {
92+
map = new ContextMap();
93+
TSUserArgSet(container, config.user_arg_index, static_cast<void *>(map));
94+
}
95+
map->set(config.method.name, ctx);
4796
}
4897

4998
JAxContext *
5099
get_user_arg(void *container, PluginConfig &config)
51100
{
52-
return static_cast<JAxContext *>(TSUserArgGet(container, config.user_arg_index));
101+
ContextMap *map = static_cast<ContextMap *>(TSUserArgGet(container, config.user_arg_index));
102+
if (map == nullptr) {
103+
return nullptr;
104+
}
105+
return map->get(config.method.name);
106+
}
107+
108+
void
109+
cleanup_user_arg(void *container, PluginConfig &config)
110+
{
111+
ContextMap *map = static_cast<ContextMap *>(TSUserArgGet(container, config.user_arg_index));
112+
if (map != nullptr) {
113+
// Remove this plugin's context from the map.
114+
map->remove(config.method.name);
115+
116+
// If the map is now empty, delete it and clear the user arg.
117+
if (map->empty()) {
118+
delete map;
119+
TSUserArgSet(container, config.user_arg_index, nullptr);
120+
}
121+
}
53122
}

plugins/experimental/jax_fingerprint/userarg.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@
3030
int reserve_user_arg(PluginConfig &config);
3131
void set_user_arg(void *container, PluginConfig &config, JAxContext *ctx);
3232
JAxContext *get_user_arg(void *container, PluginConfig &config);
33+
void cleanup_user_arg(void *container, PluginConfig &config);

0 commit comments

Comments
 (0)