This is an automated email from the ASF dual-hosted git repository.

jcoglan pushed a commit to branch mango-match-failures
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit e815f0cde19234b174124fe435e845e32b0e1f78
Author: James Coglan <[email protected]>
AuthorDate: Mon Jun 1 17:19:16 2026 +0100

    feat: Validate JS and Mango `validate_doc_update` on PUT /db/_design/doc
    
    Currently, when a design doc is updated, we validate the `map` and
    `reduce` fields, but not `validate_doc_update`. Instead, trying to
    update any other doc while an invalid `validate_doc_update` exists will
    trigger an error.
    
    This comment makes VDU validation more 'eager' by performing it when the
    ddoc itself is updated. Normal doc writes will still trigger an error if
    an invalid `validate_doc_update` already exists, but now we try to
    prevent this happening by validating VDUs when they are first created.
---
 rel/overlay/etc/default.ini                        |  7 +++
 share/server/dispatch-quickjs.js                   |  3 ++
 share/server/loop.js                               |  1 +
 share/server/state.js                              | 28 +++++++----
 src/couch/src/couch_query_servers.erl              |  7 ++-
 src/couch_mrview/src/couch_mrview.erl              | 28 ++++++++++-
 .../test/eunit/couch_scanner_test.erl              |  2 +-
 src/docs/src/config/couchdb.rst                    | 13 ++++++
 src/mango/src/mango_native_proc.erl                | 38 ++++++++++++---
 test/elixir/test/config/suite.elixir               |  3 ++
 test/elixir/test/validate_doc_update_test.exs      | 54 +++++++++++++++++++++-
 11 files changed, 162 insertions(+), 22 deletions(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 0e0eaba6b..f3f9842b4 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -132,6 +132,13 @@ view_index_dir = {{view_index_dir}}
 ; Javascript engine. The choices are: spidermonkey and quickjs
 ;js_engine = spidermonkey
 
+; When set to "true", the `validate_doc_update` field will be validated when
+; design documents are updated. For `javascript` design docs, the field must
+; contain a well-formed JavaScript function, and for `query` design docs it
+; must contain a Mango selector that is correctly structured to validate
+; document updates.
+;validate_vdu = false
+
 ; Use cfile. This is a C-based file I/O module that can execute parallel file
 ; read calls. The regular Erlang VM file module, at least as of OTP 28 forces
 ; all file operations to go through a single controlling process which can
diff --git a/share/server/dispatch-quickjs.js b/share/server/dispatch-quickjs.js
index 2f05118b3..fc5fccdd8 100644
--- a/share/server/dispatch-quickjs.js
+++ b/share/server/dispatch-quickjs.js
@@ -162,6 +162,9 @@ globalThis.dispatch = function(line) {
     case "reset":
       State.reset.apply(null, cmd);
       break;
+    case "validate_fun":
+      State.validateFun.apply(null, cmd);
+      break;
     case "add_fun":
       State.addFun.apply(null, cmd);
       break;
diff --git a/share/server/loop.js b/share/server/loop.js
index 1a5cab843..c3872aeca 100644
--- a/share/server/loop.js
+++ b/share/server/loop.js
@@ -126,6 +126,7 @@ var Loop = function() {
     "ddoc"     : DDoc.ddoc,
     // "view"    : Views.handler,
     "reset"    : State.reset,
+    "validate_fun": State.validateFun,
     "add_fun"  : State.addFun,
     "add_lib"  : State.addLib,
     "map_doc"  : Views.mapDoc,
diff --git a/share/server/state.js b/share/server/state.js
index a9b2f7ea0..5cefcfc8c 100644
--- a/share/server/state.js
+++ b/share/server/state.js
@@ -10,6 +10,18 @@
 // License for the specific language governing permissions and limitations 
under
 // the License.
 
+var makefun = function(newFun, option) {
+  switch (option) {
+    case 'nouveau':
+      var sandbox = create_nouveau_sandbox();
+      break;
+    default:
+      var sandbox = create_dreyfus_sandbox();
+      break;
+  }
+  return Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, 
sandbox);
+}
+
 var State = {
   reset : function(config) {
     // clear the globals and run gc
@@ -19,17 +31,15 @@ var State = {
     gc();
     print("true"); // indicates success
   },
+  validateFun : function(newFun, option) {
+    // Validate a function but do not store it
+    makefun(newFun, option);
+    print("true");
+  },
   addFun : function(newFun, option) {
     // Compile to a function and add it to funs array
-    switch (option) {
-      case 'nouveau':
-        var sandbox = create_nouveau_sandbox();
-        break;
-      default:
-        var sandbox = create_dreyfus_sandbox();
-        break;
-    }
-    State.funs.push(Couch.compileFunction(newFun, {views : {lib : State.lib}}, 
undefined, sandbox));
+    var fun = makefun(newFun, option);
+    State.funs.push(fun);
     print("true");
   },
   addLib : function(lib) {
diff --git a/src/couch/src/couch_query_servers.erl 
b/src/couch/src/couch_query_servers.erl
index 7ab662f85..4acbee6f2 100644
--- a/src/couch/src/couch_query_servers.erl
+++ b/src/couch/src/couch_query_servers.erl
@@ -42,7 +42,12 @@
 
 try_compile(Proc, FunctionType, FunctionName, FunctionSource) ->
     try
-        proc_prompt(Proc, [<<"add_fun">>, FunctionSource]),
+        case FunctionType of
+            validate_doc_update ->
+                proc_prompt(Proc, [<<"validate_fun">>, FunctionSource]);
+            _ ->
+                proc_prompt(Proc, [<<"add_fun">>, FunctionSource])
+        end,
         ok
     catch
         {compilation_error, E} ->
diff --git a/src/couch_mrview/src/couch_mrview.erl 
b/src/couch_mrview/src/couch_mrview.erl
index 244f668af..bf1c7e4b2 100644
--- a/src/couch_mrview/src/couch_mrview.erl
+++ b/src/couch_mrview/src/couch_mrview.erl
@@ -248,12 +248,15 @@ validate(Db, DDoc) ->
             ok
     end,
 
-    try Views =/= [] andalso couch_query_servers:get_os_process(Lang) of
+    VDU = should_validate_vdu(DDoc),
+
+    try (Views =/= [] orelse VDU =/= nil) andalso 
couch_query_servers:get_os_process(Lang) of
         false ->
             ok;
         Proc ->
             try
-                lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views)
+                lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views),
+                validate_vdu(Proc, VDU)
             after
                 couch_query_servers:ret_os_process(Proc)
             end
@@ -263,6 +266,27 @@ validate(Db, DDoc) ->
             ok
     end.
 
+validate_vdu(Proc, VDU0) ->
+    case VDU0 of
+        {ok, VDU} ->
+            couch_query_servers:try_compile(
+                Proc, validate_doc_update, <<"validate_doc_update">>, VDU
+            );
+        _ ->
+            ok
+    end.
+
+should_validate_vdu(#doc{body = {Props}}) ->
+    case config:get_boolean("couchdb", "validate_vdu", false) of
+        true ->
+            case couch_util:get_value(<<"validate_doc_update">>, Props) of
+                undefined -> nil;
+                VDU -> {ok, VDU}
+            end;
+        _ ->
+            nil
+    end.
+
 check_rank(<<N/binary>>) ->
     try binary_to_integer(N) of
         Val when Val >= 1 andalso Val =< ?MAX_RANK ->
diff --git a/src/couch_scanner/test/eunit/couch_scanner_test.erl 
b/src/couch_scanner/test/eunit/couch_scanner_test.erl
index d16183a1b..fb3705d49 100644
--- a/src/couch_scanner/test/eunit/couch_scanner_test.erl
+++ b/src/couch_scanner/test/eunit/couch_scanner_test.erl
@@ -96,7 +96,7 @@ setup() ->
             #{from => <<"x">>, to => <<"y">>}
         ],
         updates => #{u1 => <<"function(d,r){return [];}">>},
-        validate_doc_update => <<"function(n,o,u,s){return true;">>
+        validate_doc_update => <<"function(n,o,u,s){return true;}">>
     }),
     ok = add_doc(DbName2, ?DOC3, #{foo3 => bax}),
     ok = add_doc(DbName2, ?DOC4, #{foo4 => baw, <<>> => 
this_is_ok_apparently}),
diff --git a/src/docs/src/config/couchdb.rst b/src/docs/src/config/couchdb.rst
index b07ea5a30..538899881 100644
--- a/src/docs/src/config/couchdb.rst
+++ b/src/docs/src/config/couchdb.rst
@@ -258,6 +258,19 @@ Base CouchDB Options
             [couchdb]
             js_engine = spidermonkey
 
+    .. config:option:: validate_vdu :: Enable checking of 
``validate_doc_update``
+
+        .. versionadded:: TODO
+
+        When set to ``true``, the ``validate_doc_update`` field will be
+        validated when design documents are updated. For ``javascript`` design
+        docs, the field must contain a well-formed JavaScript function, and for
+        ``query`` design docs it must contain a Mango selector that is 
correctly
+        structured to validate document updates. ::
+
+            [couchdb]
+            validate_vdu = true
+
     .. config:option:: time_seq_min_time :: Minimum time-seq threshold
 
         .. versionchanged:: 3.6
diff --git a/src/mango/src/mango_native_proc.erl 
b/src/mango/src/mango_native_proc.erl
index 92a42e21c..68ddd88c1 100644
--- a/src/mango/src/mango_native_proc.erl
+++ b/src/mango/src/mango_native_proc.erl
@@ -46,7 +46,12 @@ set_timeout(Pid, TimeOut) when is_integer(TimeOut), TimeOut 
> 0 ->
     gen_server:call(Pid, {set_timeout, TimeOut}).
 
 prompt(Pid, Data) ->
-    gen_server:call(Pid, {prompt, Data}).
+    case gen_server:call(Pid, {prompt, Data}) of
+        {error, Error} ->
+            throw(Error);
+        Other ->
+            Other
+    end.
 
 init(_) ->
     {ok, #st{}}.
@@ -95,6 +100,17 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, 
_From, St) ->
                 Else
         end,
     {reply, Vals, St};
+handle_call({prompt, [<<"validate_fun">>, Selector0 | _Rest]}, _From, St) ->
+    try mango_selector:normalize(Selector0) of
+        Selector ->
+            case validate_vdu(Selector) of
+                ok -> {reply, true, St};
+                Error -> {reply, {error, Error}, St}
+            end
+    catch
+        throw:{mango_error, mango_selector, Error} ->
+            {reply, {error, Error}, St}
+    end;
 handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) ->
     NewSt =
         case couch_util:get_value(<<"validate_doc_update">>, DDoc) of
@@ -112,12 +128,10 @@ handle_call({prompt, [<<"ddoc">>, DDocId, 
[<<"validate_doc_update">>], Args]}, _
             Msg = [<<"validate_doc_update">>, DDocId],
             {stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
         Selector ->
-            case mango_selector:has_allowed_fields(Selector, [<<"newDoc">>, 
<<"oldDoc">>]) of
-                false ->
-                    Msg =
-                        <<"'validate_doc_update' may only contain 'newDoc' and 
'oldDoc' as top-level fields">>,
-                    {stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
-                true ->
+            case validate_vdu(Selector) of
+                {_, Error} ->
+                    {stop, {invalid_call, Error}, {invalid_call, Error}, St};
+                ok ->
                     [NewDoc, OldDoc, _Ctx, _SecObj] = Args,
                     Struct =
                         case OldDoc of
@@ -137,6 +151,16 @@ handle_call({prompt, [<<"ddoc">>, DDocId, 
[<<"validate_doc_update">>], Args]}, _
 handle_call(Msg, _From, St) ->
     {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
 
+validate_vdu(VDU) ->
+    case mango_selector:has_allowed_fields(VDU, [<<"newDoc">>, <<"oldDoc">>]) 
of
+        true ->
+            ok;
+        false ->
+            Msg =
+                <<"'validate_doc_update' may only contain 'newDoc' and 
'oldDoc' as top-level fields">>,
+            {compilation_error, Msg}
+    end.
+
 handle_cast(garbage_collect, St) ->
     garbage_collect(),
     {noreply, St};
diff --git a/test/elixir/test/config/suite.elixir 
b/test/elixir/test/config/suite.elixir
index 57cf3334f..1ea321414 100644
--- a/test/elixir/test/config/suite.elixir
+++ b/test/elixir/test/config/suite.elixir
@@ -526,12 +526,15 @@
     "JavaScript VDU rejects an invalid document",
     "JavaScript VDU accepts a valid change",
     "JavaScript VDU rejects an invalid change",
+    "invalid JavaScript VDU is detected on doc update",
+    "invalid JavaScript VDU is rejected on design doc update",
     "Mango VDU accepts a valid document",
     "Mango VDU rejects an invalid document",
     "updating a Mango VDU updates its effects",
     "converting a Mango VDU to JavaScript updates its effects",
     "deleting a Mango VDU removes its effects",
     "Mango VDU rejects a doc if any existing ddoc fails to match",
+    "invalid Mango VDU is detected on doc update",
     "Mango VDU rejects a design doc if it contains unknown fields",
   ],
   "SecurityValidationTest": [
diff --git a/test/elixir/test/validate_doc_update_test.exs 
b/test/elixir/test/validate_doc_update_test.exs
index 0ebb91342..502630404 100644
--- a/test/elixir/test/validate_doc_update_test.exs
+++ b/test/elixir/test/validate_doc_update_test.exs
@@ -77,6 +77,36 @@ defmodule ValidateDocUpdateTest do
     assert resp.status_code == 403
   end
 
+  @tag :with_db
+  test "invalid JavaScript VDU is detected on doc update", context do
+    set_config({"couchdb", "validate_vdu", "false"})
+    db = context[:db_name]
+
+    resp = Couch.put("/#{db}/_design/js-test", body: %{
+      language: "javascript",
+      validate_doc_update: "function () {"
+    })
+
+    assert resp.status_code == 201
+
+    resp = Couch.put("/#{db}/doc", body: %{a: 1})
+    assert resp.status_code == 500
+  end
+
+  @tag :with_db
+  test "invalid JavaScript VDU is rejected on design doc update", context do
+    set_config({"couchdb", "validate_vdu", "true"})
+    db = context[:db_name]
+
+    resp = Couch.put("/#{db}/_design/js-test", body: %{
+      language: "javascript",
+      validate_doc_update: "function () {"
+    })
+
+    assert resp.status_code == 400
+    assert resp.body["error"] == "compilation_error"
+  end
+
   @mango_type_check %{
     language: "query",
 
@@ -216,7 +246,8 @@ defmodule ValidateDocUpdateTest do
   end
 
   @tag :with_db
-  test "Mango VDU rejects a design doc if it contains unknown fields", context 
do
+  test "invalid Mango VDU is detected on doc update", context do
+    set_config({"couchdb", "validate_vdu", "false"})
     db = context[:db_name]
 
     ddoc = %{
@@ -226,10 +257,29 @@ defmodule ValidateDocUpdateTest do
         "wrongField" => %{"year" => %{"$lt" => 2026}}
       }
     }
+
     resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc)
     assert resp.status_code == 201
 
-    resp = Couch.put("/#{db}/doc", body: %{"year" => 1994})
+    resp = Couch.put("/#{db}/doc", body: %{a: 1})
     assert resp.status_code == 500
   end
+
+  @tag :with_db
+  test "Mango VDU rejects a design doc if it contains unknown fields", context 
do
+    set_config({"couchdb", "validate_vdu", "true"})
+    db = context[:db_name]
+
+    ddoc = %{
+      language: "query",
+
+      validate_doc_update: %{
+        "wrongField" => %{"year" => %{"$lt" => 2026}}
+      }
+    }
+
+    resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc)
+    assert resp.status_code == 400
+    assert resp.body["error"] == "compilation_error"
+  end
 end

Reply via email to