Remoting Callables

Remoting is the library implementing communication between Jenkins processes using Java serialization. This applies to controller/agent communications and agents communicating with the Maven process in Maven Integration Plugin.

hudson.remoting.Callable is the basic building block of this communication channel, representing a message and corresponding response. Objects implementing this interface get serialized and sent to the other end of the channel. They’re deserialized on the other end, where the #call() method is invoked. Its return value is then serialized and sent back through the channel.

Think of each Callable implementation as an interface or API of Jenkins: Jenkins will execute Callables it receives through a remoting channel, with fields in the Callable subtype being like parameters.

Remoting roles

It is important to ensure that the less-privileged end of the channel (typically agents) cannot run arbitrary code on the more-privileged end (typically a controller), so care needs to be taken when implementing Callables. The remoting library offers the RoleSensitive interface for this which Callable extends since Jenkins 1.587 and 1.580.1: Callables can use it to limit where they can be executed by implementing #checkRoles(RoleChecker).

The abstract classes MasterToSlaveCallable and SlaveToMasterCallable implement this interface with the two most common modes:

  • MasterToSlaveCallable can be sent from a controller to an agent. In general, you should write your code so it works with this implementation.

  • SlaveToMasterCallable can be sent from an agent to a controller. This is less safe, as malicious agents can use these implementations to run code on a controller.

Despite their names, either implementation can be executed anywhere, the name just describes through which channels it can be sent for execution: MasterToSlaveCallable can be executed on the controller, just not sent from an agent to a controller for execution, unlike SlaveToMasterCallable, which can be sent through a controller/agent channel through either direction.

In addition to the above, NotReallyRoleSensitiveCallable can be used for a Callable that is not intended to be sent through a channel. Its #checkRoles method will throw an exception, thereby preventing it from being executed on any side of a communication channel that performs a role check.

It is recommended to use one of these classes in your plugin, rather than implementing #checkRoles(RoleChecker) directly.

Mandatory role checks

Since Jenkins 2.319 and LTS 2.303.3, Callable implementations providing an implementation of #checkRoles(RoleChecker) that neither throws an exception nor calls RoleChecker#check will result in the Callable being rejected when sent through the channel to a side that requires a role check before execution. A typical implementation that breaks will look like the following:

class MyCallable implements Callable<ReturnType,ExceptionType> {
    private String parameter;
    public MyCallable(String parameter) {
        this.parameter = parameter;
    }
    public ReturnType call() throws ExceptionType {
        return Some.code().operatesOn(this.parameter);
    }

    public void checkRoles(RoleChecker checker) {
        // this is an empty block (1)
    }
}
1 Nothing is being done here even though a call to RoleChecker#check(…​) is expected.

If a plugin implementing an inadequate role check (like this example) attempts to send a Callable through the remoting channel from the agent to the controller, the security improvement will detect it and throw an exception before #call() would be invoked.

While administrators can allow specific Callable subtypes to bypass this protection mechanism (see documentation), plugin developers are advised to update their plugins: Callable implementations should extend from one of the classes mentioned above, with a strong preference for MasterToSlaveCallable, which should work in almost all cases. If that does not work and the plugin cannot be restructured to work with a MasterToSlaveCallable, the Callable implementation should be changed to a SlaveToMasterCallable, and the best practices recommended below must be implemented to ensure it cannot be bypassed.

Best practices

Prohibit sending to controllers wherever possible

Use MasterToSlaveCallable wherever possible, restructure your plugin code as needed to allow this.

For Callables not intended to be sent through a channel, extend NotReallyRoleSensitiveCallable instead of implementing Callable directly.

Never run unsafe code during deserialization or role check

All Callables are still deserialized on the controller, and #checkRoles is invoked to determine whether the Callable can be executed. Do not add code to #readResolve and related methods, or to #checkRoles, that would be unsafe to execute, as these methods will still be executed on the controller between receiving the Callable and invoking #call().

Minimal, safe implementations

If a Callable needs to be sent from an agent to a controller (SlaveToMasterCallable), keep the implementations minimal: Use few, simple parameters that are easy to reason about and validate, do not send complex trees of objects. Do not use (anonymous) inner classes for your Callables, instead make them top-level or static nested classes so they do not have a reference to an instance of the surrounding class, reducing complexity. Perform parameter validation inside #call() or #readResolve() rather than (only) in constructors and setters, and do not rely on private or final modifiers for your fields to protect from unexpected values: reflection can be used to set fields to arbitrary, attacker-chosen values.

The vast majority of plugins should have their Callables extend from MasterToSlaveCallable (if they’re implementing Callable) or MasterToSlaveFileCallable (if they’re implementing FileCallable).

This will allow the controller-side of a connection to initiate the Callable submission and invocation on both the controller and any agents and is the safest approach.

This is mostly equivalent to implementing checkRoles as follows:

    public void checkRoles(RoleChecker checker) {
        checker.check(this,Roles.SLAVE);
    }
}

In very care cases, the agent-side of a connection will send a Callable to the controller for execution there. This is tricky to do safely and it’s generally recommended plugins are restructured so this isn’t needed. If this isn’t possible, plugins can implement SlaveToMasterCallable or SlaveToMasterFileCallable, or implement #checkRoles as follows:

    public void checkRoles(RoleChecker checker) {
        checker.check(this,Roles.MASTER);
    }
}

Make sure to carefully consider what your Callable implementation is able to do on the controller when sent from an untrusted agent. Carefully review the best practices above and adapt your Callable accordingly.