diff --git a/doc/whats_new/v0.22.rst b/doc/whats_new/v0.22.rst
index d1459bcb8caf3..de0572b9ffd7d 100644
--- a/doc/whats_new/v0.22.rst
+++ b/doc/whats_new/v0.22.rst
@@ -47,6 +47,11 @@ Changelog
   validation data separately to avoid any data leak. :pr:`13933` by
   `NicolasHug`_.

+- |Feature| :class:`ensemble.HistGradientBoostingClassifier` and
+  :class:`ensemble.HistGradientBoostingRegressor` have an additional
+  parameter called `warm_start` that enables warm starting. :pr:`14012` by
+  :user:`Johann Faouzi <johannfaouzi>`.
+
 :mod:`sklearn.linear_model`
 ..................

diff --git a/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py b/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py
index 466181f445ee8..620e8afe5f276 100644
--- a/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py
+++ b/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py
@@ -26,8 +26,8 @@ class BaseHistGradientBoosting(BaseEstimator, ABC):
     @abstractmethod
     def __init__(self, loss, learning_rate, max_iter, max_leaf_nodes,
                  max_depth, min_samples_leaf, l2_regularization, max_bins,
-                 scoring, validation_fraction, n_iter_no_change, tol, verbose,
-                 random_state):
+                 warm_start, scoring, validation_fraction, n_iter_no_change,
+                 tol, verbose, random_state):
         self.loss = loss
         self.learning_rate = learning_rate
         self.max_iter = max_iter
@@ -36,9 +36,10 @@ def __init__(self, loss, learning_rate, max_iter, max_leaf_nodes,
         self.min_samples_leaf = min_samples_leaf
         self.l2_regularization = l2_regularization
         self.max_bins = max_bins
-        self.n_iter_no_change = n_iter_no_change
-        self.validation_fraction = validation_fraction
+        self.warm_start = warm_start
         self.scoring = scoring
+        self.validation_fraction = validation_fraction
+        self.n_iter_no_change = n_iter_no_change
         self.tol = tol
         self.verbose = verbose
         self.random_state = random_state
@@ -88,7 +89,6 @@ def fit(self, X, y):
         -------
         self : object
         """
-
         fit_start_time = time()
         acc_find_split_time = 0.  # time spent finding the best splits
         acc_apply_split_time = 0.  # time spent splitting nodes
@@ -97,7 +97,13 @@ def fit(self, X, y):
         acc_prediction_time = 0.
         X, y = check_X_y(X, y, dtype=[X_DTYPE])
         y = self._encode_y(y)
-        rng = check_random_state(self.random_state)
+
+        # The rng state must be preserved if warm_start is True
+        if (self.warm_start and hasattr(self, '_rng')):
+            rng = self._rng
+        else:
+            rng = check_random_state(self.random_state)
+            self._rng = rng

         self._validate_parameters()
         self.n_features_ = X.shape[1]  # used for validation in predict()
@@ -112,7 +118,6 @@ def fit(self, X, y):
         # data.
         self._in_fit = True

-
         self.loss_ = self._get_loss()

         self.do_early_stopping_ = (self.n_iter_no_change is not None and
@@ -124,9 +129,15 @@ def fit(self, X, y):
             # stratify for classification
             stratify = y if hasattr(self.loss_, 'predict_proba') else None

+            # Save the state of the RNG for the training and validation split.
+            # This is needed in order to have the same split when using
+            # warm starting.
+            if not (self._is_fitted() and self.warm_start):
+                self._train_val_split_seed = rng.randint(1024)
+
             X_train, X_val, y_train, y_val = train_test_split(
                 X, y, test_size=self.validation_fraction, stratify=stratify,
-                random_state=rng)
+                random_state=self._train_val_split_seed)
         else:
             X_train, y_train = X, y
             X_val, y_val = None, None
@@ -142,86 +153,127 @@ def fit(self, X, y):
         if self.verbose:
             print("Fitting gradient boosted rounds:")

-        # initialize raw_predictions: those are the accumulated values
-        # predicted by the trees for the training data. raw_predictions has
-        # shape (n_trees_per_iteration, n_samples) where
-        # n_trees_per_iterations is n_classes in multiclass classification,
-        # else 1.
         n_samples = X_binned_train.shape[0]
-        self._baseline_prediction = self.loss_.get_baseline_prediction(
-            y_train, self.n_trees_per_iteration_
-        )
-        raw_predictions = np.zeros(
-            shape=(self.n_trees_per_iteration_, n_samples),
-            dtype=self._baseline_prediction.dtype
-        )
-        raw_predictions += self._baseline_prediction

-        # initialize gradients and hessians (empty arrays).
-        # shape = (n_trees_per_iteration, n_samples).
-        gradients, hessians = self.loss_.init_gradients_and_hessians(
-            n_samples=n_samples,
-            prediction_dim=self.n_trees_per_iteration_
-        )
+        # First time calling fit, or no warm start
+        if not (self._is_fitted() and self.warm_start):
+            # Clear random state and score attributes
+            self._clear_state()
+
+            # initialize raw_predictions: those are the accumulated values
+            # predicted by the trees for the training data. raw_predictions has
+            # shape (n_trees_per_iteration, n_samples) where
+            # n_trees_per_iterations is n_classes in multiclass classification,
+            # else 1.
+            self._baseline_prediction = self.loss_.get_baseline_prediction(
+                y_train, self.n_trees_per_iteration_
+            )
+            raw_predictions = np.zeros(
+                shape=(self.n_trees_per_iteration_, n_samples),
+                dtype=self._baseline_prediction.dtype
+            )
+            raw_predictions += self._baseline_prediction

-        # predictors is a matrix (list of lists) of TreePredictor objects
-        # with shape (n_iter_, n_trees_per_iteration)
-        self._predictors = predictors = []
+            # initialize gradients and hessians (empty arrays).
+            # shape = (n_trees_per_iteration, n_samples).
+            gradients, hessians = self.loss_.init_gradients_and_hessians(
+                n_samples=n_samples,
+                prediction_dim=self.n_trees_per_iteration_
+            )

-        # Initialize structures and attributes related to early stopping
-        self.scorer_ = None  # set if scoring != loss
-        raw_predictions_val = None  # set if scoring == loss and use val
-        self.train_score_ = []
-        self.validation_score_ = []
-        if self.do_early_stopping_:
-            # populate train_score and validation_score with the predictions
-            # of the initial model (before the first tree)
+            # predictors is a matrix (list of lists) of TreePredictor objects
+            # with shape (n_iter_, n_trees_per_iteration)
+            self._predictors = predictors = []

-            if self.scoring == 'loss':
-                # we're going to compute scoring w.r.t the loss. As losses
-                # take raw predictions as input (unlike the scorers), we can
-                # optimize a bit and avoid repeating computing the predictions
-                # of the previous trees. We'll re-use raw_predictions (as it's
-                # needed for training anyway) for evaluating the training
-                # loss, and create raw_predictions_val for storing the
-                # raw predictions of the validation data.
-
-                if self._use_validation_data:
-                    raw_predictions_val = np.zeros(
-                        shape=(self.n_trees_per_iteration_,
-                               X_binned_val.shape[0]),
-                        dtype=self._baseline_prediction.dtype
-                    )
+            # Initialize structures and attributes related to early stopping
+            self.scorer_ = None  # set if scoring != loss
+            raw_predictions_val = None  # set if scoring == loss and use val
+            self.train_score_ = []
+            self.validation_score_ = []
+
+            if self.do_early_stopping_:
+                # populate train_score and validation_score with the
+                # predictions of the initial model (before the first tree)

-                    raw_predictions_val += self._baseline_prediction
+                if self.scoring == 'loss':
+                    # we're going to compute scoring w.r.t the loss. As losses
+                    # take raw predictions as input (unlike the scorers), we
+                    # can optimize a bit and avoid repeating computing the
+                    # predictions of the previous trees. We'll re-use
+                    # raw_predictions (as it's needed for training anyway) for
+                    # evaluating the training loss, and create
+                    # raw_predictions_val for storing the raw predictions of
+                    # the validation data.

-                self._check_early_stopping_loss(raw_predictions, y_train,
-                                                raw_predictions_val, y_val)
-            else:
-                self.scorer_ = check_scoring(self, self.scoring)
-                # scorer_ is a callable with signature (est, X, y) and calls
-                # est.predict() or est.predict_proba() depending on its nature.
-                # Unfortunately, each call to scorer_() will compute
-                # the predictions of all the trees. So we use a subset of the
-                # training set to compute train scores.
-                subsample_size = 10000  # should we expose this parameter?
-                indices = np.arange(X_binned_train.shape[0])
-                if X_binned_train.shape[0] > subsample_size:
-                    # TODO: not critical but stratify using resample()
-                    indices = rng.choice(indices, subsample_size,
-                                         replace=False)
-                X_binned_small_train = X_binned_train[indices]
-                y_small_train = y_train[indices]
-                # Predicting is faster on C-contiguous arrays.
-                X_binned_small_train = np.ascontiguousarray(
-                    X_binned_small_train)
-
-                self._check_early_stopping_scorer(
-                    X_binned_small_train, y_small_train,
-                    X_binned_val, y_val,
+                    if self._use_validation_data:
+                        raw_predictions_val = np.zeros(
+                            shape=(self.n_trees_per_iteration_,
+                                   X_binned_val.shape[0]),
+                            dtype=self._baseline_prediction.dtype
+                        )
+
+                        raw_predictions_val += self._baseline_prediction
+
+                    self._check_early_stopping_loss(raw_predictions, y_train,
+                                                    raw_predictions_val, y_val)
+                else:
+                    self.scorer_ = check_scoring(self, self.scoring)
+                    # scorer_ is a callable with signature (est, X, y) and
+                    # calls est.predict() or est.predict_proba() depending on
+                    # its nature.
+                    # Unfortunately, each call to scorer_() will compute
+                    # the predictions of all the trees. So we use a subset of
+                    # the training set to compute train scores.
+
+                    # Save the seed for the small trainset generator
+                    self._small_trainset_seed = rng.randint(1024)
+
+                    # Compute the subsample set
+                    (X_binned_small_train,
+                     y_small_train) = self._get_small_trainset(
+                        X_binned_train, y_train, self._small_trainset_seed)
+
+                    self._check_early_stopping_scorer(
+                        X_binned_small_train, y_small_train,
+                        X_binned_val, y_val,
+                    )
+            begin_at_stage = 0
+
+        # warm start: this is not the first time fit was called
+        else:
+            # Check that the maximum number of iterations is not smaller
+            # than the number of iterations from the previous fit
+            if self.max_iter < self.n_iter_:
+                raise ValueError(
+                    'max_iter=%d must be larger than or equal to '
+                    'n_iter_=%d when warm_start==True'
+                    % (self.max_iter, self.n_iter_)
                 )

-        for iteration in range(self.max_iter):
+            # Convert array attributes to lists
+            self.train_score_ = self.train_score_.tolist()
+            self.validation_score_ = self.validation_score_.tolist()
+
+            # Compute raw predictions
+            raw_predictions = self._raw_predict(X_binned_train)
+
+            if self.do_early_stopping_ and self.scoring != 'loss':
+                # Compute the subsample set
+                X_binned_small_train, y_small_train = self._get_small_trainset(
+                    X_binned_train, y_train, self._small_trainset_seed)
+
+            # Initialize the gradients and hessians
+            gradients, hessians = self.loss_.init_gradients_and_hessians(
+                n_samples=n_samples,
+                prediction_dim=self.n_trees_per_iteration_
+            )
+
+            # Get the predictors from the previous fit
+            predictors = self._predictors
+
+            begin_at_stage = self.n_iter_
+
+        for iteration in range(begin_at_stage, self.max_iter):

             if self.verbose:
                 iteration_start_time = time()
@@ -318,13 +370,38 @@ def fit(self, X, y):
         del self._in_fit  # hard delete so we're sure it can't be used anymore
         return self

+    def _is_fitted(self):
+        return len(getattr(self, '_predictors', [])) > 0
+
+    def _clear_state(self):
+        """Clear the state of the gradient boosting model."""
+        for var in ('train_score_', 'validation_score_', '_rng'):
+            if hasattr(self, var):
+                delattr(self, var)
+
+    def _get_small_trainset(self, X_binned_train, y_train, seed):
+        """Compute the indices of the subsample set and return this set.
+
+        For efficiency, we need to subsample the training set to compute scores
+        with scorers.
+        """
+        subsample_size = 10000
+        rng = check_random_state(seed)
+        indices = np.arange(X_binned_train.shape[0])
+        if X_binned_train.shape[0] > subsample_size:
+            # TODO: not critical but stratify using resample()
+            indices = rng.choice(indices, subsample_size, replace=False)
+        X_binned_small_train = X_binned_train[indices]
+        y_small_train = y_train[indices]
+        X_binned_small_train = np.ascontiguousarray(X_binned_small_train)
+        return X_binned_small_train, y_small_train
+
     def _check_early_stopping_scorer(self, X_binned_small_train, y_small_train,
                                      X_binned_val, y_val):
         """Check if fitting should be early-stopped based on scorer.

         Scores are computed on validation data or on training data.
         """
-
         self.train_score_.append(
             self.scorer_(self, X_binned_small_train, y_small_train)
         )
@@ -555,6 +632,11 @@ class HistGradientBoostingRegressor(BaseHistGradientBoosting, RegressorMixin):
         allows for a much faster training stage. Features with a small
         number of unique values may use less than ``max_bins`` bins. Must be no
         larger than 256.
+    warm_start : bool, optional (default=False)
+        When set to ``True``, reuse the solution of the previous call to fit
+        and add more estimators to the ensemble. For results to be valid, the
+        estimator should be re-trained on the same data only.
+        See :term:`the Glossary <warm_start>`.
     scoring : str or callable or None, optional (default=None)
         Scoring parameter to use for early stopping. It can be a single
         string (see :ref:`scoring_parameter`) or a callable (see
@@ -568,7 +650,7 @@ class HistGradientBoostingRegressor(BaseHistGradientBoosting, RegressorMixin):
     n_iter_no_change : int or None, optional (default=None)
         Used to determine when to "early stop". The fitting process is
         stopped when none of the last ``n_iter_no_change`` scores are better
-        than the ``n_iter_no_change - 1``th-to-last one, up to some
+        than the ``n_iter_no_change - 1`` -th-to-last one, up to some
         tolerance. If None or 0, no early-stopping is done.
     tol : float or None, optional (default=1e-7)
         The absolute tolerance to use when comparing scores during early
@@ -592,13 +674,13 @@ class HistGradientBoostingRegressor(BaseHistGradientBoosting, RegressorMixin):
     n_trees_per_iteration_ : int
         The number of tree that are built at each iteration. For regressors,
         this is always 1.
-    train_score_ : ndarray, shape (max_iter + 1,)
+    train_score_ : ndarray, shape (n_iter_ + 1,)
         The scores at each iteration on the training data. The first entry
         is the score of the ensemble before the first iteration. Scores are
         computed according to the ``scoring`` parameter. If ``scoring`` is
         not 'loss', scores are computed on a subset of at most 10 000
         samples. Empty if no early stopping.
-    validation_score_ : ndarray, shape (max_iter + 1,)
+    validation_score_ : ndarray, shape (n_iter_ + 1,)
         The scores at each iteration on the held-out validation data. The
         first entry is the score of the ensemble before the first iteration.
         Scores are computed according to the ``scoring`` parameter. Empty if
@@ -621,14 +703,16 @@ class HistGradientBoostingRegressor(BaseHistGradientBoosting, RegressorMixin):
     def __init__(self, loss='least_squares', learning_rate=0.1,
                  max_iter=100, max_leaf_nodes=31, max_depth=None,
                  min_samples_leaf=20, l2_regularization=0., max_bins=256,
-                 scoring=None, validation_fraction=0.1, n_iter_no_change=None,
-                 tol=1e-7, verbose=0, random_state=None):
+                 warm_start=False, scoring=None, validation_fraction=0.1,
+                 n_iter_no_change=None, tol=1e-7, verbose=0,
+                 random_state=None):
         super(HistGradientBoostingRegressor, self).__init__(
             loss=loss, learning_rate=learning_rate, max_iter=max_iter,
             max_leaf_nodes=max_leaf_nodes, max_depth=max_depth,
             min_samples_leaf=min_samples_leaf,
             l2_regularization=l2_regularization, max_bins=max_bins,
-            scoring=scoring, validation_fraction=validation_fraction,
+            warm_start=warm_start, scoring=scoring,
+            validation_fraction=validation_fraction,
             n_iter_no_change=n_iter_no_change, tol=tol, verbose=verbose,
             random_state=random_state)

@@ -723,6 +807,11 @@ class HistGradientBoostingClassifier(BaseHistGradientBoosting,
         allows for a much faster training stage. Features with a small
         number of unique values may use less than ``max_bins`` bins. Must be no
         larger than 256.
+    warm_start : bool, optional (default=False)
+        When set to ``True``, reuse the solution of the previous call to fit
+        and add more estimators to the ensemble. For results to be valid, the
+        estimator should be re-trained on the same data only.
+        See :term:`the Glossary <warm_start>`.
     scoring : str or callable or None, optional (default=None)
         Scoring parameter to use for early stopping. It can be a single
         string (see :ref:`scoring_parameter`) or a callable (see
@@ -736,7 +825,7 @@ class HistGradientBoostingClassifier(BaseHistGradientBoosting,
     n_iter_no_change : int or None, optional (default=None)
         Used to determine when to "early stop". The fitting process is
         stopped when none of the last ``n_iter_no_change`` scores are better
-        than the ``n_iter_no_change - 1``th-to-last one, up to some
+        than the ``n_iter_no_change - 1`` -th-to-last one, up to some
         tolerance. If None or 0, no early-stopping is done.
     tol : float or None, optional (default=1e-7)
         The absolute tolerance to use when comparing scores. The higher the
@@ -761,13 +850,13 @@ class HistGradientBoostingClassifier(BaseHistGradientBoosting,
         The number of tree that are built at each iteration. This is equal to 1
         for binary classification, and to ``n_classes`` for multiclass
         classification.
-    train_score_ : ndarray, shape (max_iter + 1,)
+    train_score_ : ndarray, shape (n_iter_ + 1,)
         The scores at each iteration on the training data. The first entry
         is the score of the ensemble before the first iteration. Scores are
         computed according to the ``scoring`` parameter. If ``scoring`` is
         not 'loss', scores are computed on a subset of at most 10 000
         samples. Empty if no early stopping.
-    validation_score_ : ndarray, shape (max_iter + 1,)
+    validation_score_ : ndarray, shape (n_iter_ + 1,)
         The scores at each iteration on the held-out validation data. The
         first entry is the score of the ensemble before the first iteration.
         Scores are computed according to the ``scoring`` parameter. Empty if
@@ -790,15 +879,16 @@ class HistGradientBoostingClassifier(BaseHistGradientBoosting,

     def __init__(self, loss='auto', learning_rate=0.1, max_iter=100,
                  max_leaf_nodes=31, max_depth=None, min_samples_leaf=20,
-                 l2_regularization=0., max_bins=256, scoring=None,
-                 validation_fraction=0.1, n_iter_no_change=None, tol=1e-7,
-                 verbose=0, random_state=None):
+                 l2_regularization=0., max_bins=256, warm_start=False,
+                 scoring=None, validation_fraction=0.1, n_iter_no_change=None,
+                 tol=1e-7, verbose=0, random_state=None):
         super(HistGradientBoostingClassifier, self).__init__(
             loss=loss, learning_rate=learning_rate, max_iter=max_iter,
             max_leaf_nodes=max_leaf_nodes, max_depth=max_depth,
             min_samples_leaf=min_samples_leaf,
             l2_regularization=l2_regularization, max_bins=max_bins,
-            scoring=scoring, validation_fraction=validation_fraction,
+            warm_start=warm_start, scoring=scoring,
+            validation_fraction=validation_fraction,
             n_iter_no_change=n_iter_no_change, tol=tol, verbose=verbose,
             random_state=random_state)

diff --git a/sklearn/ensemble/_hist_gradient_boosting/tests/test_warm_start.py b/sklearn/ensemble/_hist_gradient_boosting/tests/test_warm_start.py
new file mode 100644
index 0000000000000..806ad94ccee98
--- /dev/null
+++ b/sklearn/ensemble/_hist_gradient_boosting/tests/test_warm_start.py
@@ -0,0 +1,190 @@
+import numpy as np
+from numpy.testing import assert_array_equal
+from numpy.testing import assert_allclose
+
+import pytest
+
+from sklearn.base import clone
+from sklearn.datasets import make_classification, make_regression
+
+# To use this experimental feature, we need to explicitly ask for it:
+from sklearn.experimental import enable_hist_gradient_boosting  # noqa
+from sklearn.ensemble import HistGradientBoostingRegressor
+from sklearn.ensemble import HistGradientBoostingClassifier
+
+
+X_classification, y_classification = make_classification(random_state=0)
+X_regression, y_regression = make_regression(random_state=0)
+
+
+def _assert_predictor_equal(gb_1, gb_2, X):
+    """Assert that two HistGBM instances are identical."""
+    # Check identical nodes for each tree
+    for (pred_ith_1, pred_ith_2) in zip(gb_1._predictors, gb_2._predictors):
+        for (predictor_1, predictor_2) in zip(pred_ith_1, pred_ith_2):
+            assert_array_equal(predictor_1.nodes, predictor_2.nodes)
+
+    # Check identical predictions
+    assert_allclose(gb_1.predict(X), gb_2.predict(X))
+
+
+@pytest.mark.parametrize('GradientBoosting, X, y', [
+    (HistGradientBoostingClassifier, X_classification, y_classification),
+    (HistGradientBoostingRegressor, X_regression, y_regression)
+])
+def test_max_iter_with_warm_start_validation(GradientBoosting, X, y):
+    # Check that a ValueError is raised when the maximum number of iterations
+    # is smaller than the number of iterations from the previous fit when warm
+    # start is True.
+
+    estimator = GradientBoosting(max_iter=50, warm_start=True)
+    estimator.fit(X, y)
+    estimator.set_params(max_iter=25)
+    err_msg = ('max_iter=25 must be larger than or equal to n_iter_=50 '
+               'when warm_start==True')
+    with pytest.raises(ValueError, match=err_msg):
+        estimator.fit(X, y)
+
+
+@pytest.mark.parametrize('GradientBoosting, X, y', [
+    (HistGradientBoostingClassifier, X_classification, y_classification),
+    (HistGradientBoostingRegressor, X_regression, y_regression)
+])
+def test_warm_start_yields_identical_results(GradientBoosting, X, y):
+    # Make sure that fitting 50 iterations and then 25 with warm start is
+    # equivalent to fitting 75 iterations.
+
+    rng = 42
+    gb_warm_start = GradientBoosting(
+        n_iter_no_change=100, max_iter=50, random_state=rng, warm_start=True
+    )
+    gb_warm_start.fit(X, y).set_params(max_iter=75).fit(X, y)
+
+    gb_no_warm_start = GradientBoosting(
+        n_iter_no_change=100, max_iter=75, random_state=rng, warm_start=False
+    )
+    gb_no_warm_start.fit(X, y)
+
+    # Check that both predictors are equal
+    _assert_predictor_equal(gb_warm_start, gb_no_warm_start, X)
+
+
+@pytest.mark.parametrize('GradientBoosting, X, y', [
+    (HistGradientBoostingClassifier, X_classification, y_classification),
+    (HistGradientBoostingRegressor, X_regression, y_regression)
+])
+def test_warm_start_max_depth(GradientBoosting, X, y):
+    # Test if possible to fit trees of different depth in ensemble.
+    gb = GradientBoosting(max_iter=100, min_samples_leaf=1,
+                          warm_start=True, max_depth=2)
+    gb.fit(X, y)
+    gb.set_params(max_iter=110, max_depth=3)
+    gb.fit(X, y)
+
+    # First 100 trees have max_depth == 2
+    for i in range(100):
+        assert gb._predictors[i][0].get_max_depth() == 2
+    # Last 10 trees have max_depth == 3
+    for i in range(1, 11):
+        assert gb._predictors[-i][0].get_max_depth() == 3
+
+
+@pytest.mark.parametrize('GradientBoosting, X, y', [
+    (HistGradientBoostingClassifier, X_classification, y_classification),
+    (HistGradientBoostingRegressor, X_regression, y_regression)
+])
+def test_warm_start_early_stopping(GradientBoosting, X, y):
+    # Make sure that early stopping occurs after a small number of iterations
+    # when fitting a second time with warm starting.
+
+    n_iter_no_change = 5
+    gb = GradientBoosting(
+        n_iter_no_change=n_iter_no_change, max_iter=10000,
+        random_state=42, warm_start=True, tol=1e-3
+    )
+    gb.fit(X, y)
+    n_iter_first_fit = gb.n_iter_
+    gb.fit(X, y)
+    n_iter_second_fit = gb.n_iter_
+    assert n_iter_second_fit - n_iter_first_fit < n_iter_no_change
+
+
+@pytest.mark.parametrize('GradientBoosting, X, y', [
+    (HistGradientBoostingClassifier, X_classification, y_classification),
+    (HistGradientBoostingRegressor, X_regression, y_regression)
+])
+def test_warm_start_equal_n_estimators(GradientBoosting, X, y):
+    # Test if warm start with equal n_estimators does nothing
+    gb_1 = GradientBoosting(max_depth=2)
+    gb_1.fit(X, y)
+
+    gb_2 = clone(gb_1)
+    gb_2.set_params(max_iter=gb_1.max_iter, warm_start=True)
+    gb_2.fit(X, y)
+
+    # Check that both predictors are equal
+    _assert_predictor_equal(gb_1, gb_2, X)
+
+
+@pytest.mark.parametrize('GradientBoosting, X, y', [
+    (HistGradientBoostingClassifier, X_classification, y_classification),
+    (HistGradientBoostingRegressor, X_regression, y_regression)
+])
+def test_warm_start_clear(GradientBoosting, X, y):
+    # Test if fit clears state.
+    gb_1 = GradientBoosting(n_iter_no_change=5, random_state=42)
+    gb_1.fit(X, y)
+
+    gb_2 = GradientBoosting(n_iter_no_change=5, random_state=42,
+                            warm_start=True)
+    gb_2.fit(X, y)  # inits state
+    gb_2.set_params(warm_start=False)
+    gb_2.fit(X, y)  # clears old state and equals est
+
+    # Check that both predictors have the same train_score_ and
+    # validation_score_ attributes
+    assert_allclose(gb_1.train_score_, gb_2.train_score_)
+    assert_allclose(gb_1.validation_score_, gb_2.validation_score_)
+
+    # Check that both predictors are equal
+    _assert_predictor_equal(gb_1, gb_2, X)
+
+
+@pytest.mark.parametrize('GradientBoosting, X, y', [
+    (HistGradientBoostingClassifier, X_classification, y_classification),
+    (HistGradientBoostingRegressor, X_regression, y_regression)
+])
+@pytest.mark.parametrize('rng_type', ('int', 'instance'))
+def test_random_seeds_warm_start(GradientBoosting, X, y, rng_type):
+    # Make sure the seeds for train/val split and small trainset subsampling
+    # are correctly set in a warm start context.
+    def _get_rng(rng_type):
+        # Helper to avoid consuming rngs
+        if rng_type == 'int':
+            return 42
+        else:
+            return np.random.RandomState(0)
+
+    random_state = _get_rng(rng_type)
+    gb_1 = GradientBoosting(n_iter_no_change=5, max_iter=2,
+                            random_state=random_state)
+    gb_1.fit(X, y)
+    train_val_seed_1 = gb_1._train_val_split_seed
+    small_trainset_seed_1 = gb_1._small_trainset_seed
+
+    random_state = _get_rng(rng_type)
+    gb_2 = GradientBoosting(n_iter_no_change=5, max_iter=2,
+                            random_state=random_state, warm_start=True)
+    gb_2.fit(X, y)  # inits state
+    train_val_seed_2 = gb_2._train_val_split_seed
+    small_trainset_seed_2 = gb_2._small_trainset_seed
+    gb_2.fit(X, y)  # clears old state and equals est
+    train_val_seed_3 = gb_2._train_val_split_seed
+    small_trainset_seed_3 = gb_2._small_trainset_seed
+
+    # Check that all seeds are equal
+    assert train_val_seed_1 == train_val_seed_2
+    assert small_trainset_seed_1 == small_trainset_seed_2
+
+    assert train_val_seed_2 == train_val_seed_3
+    assert small_trainset_seed_2 == small_trainset_seed_3

Feature request: warm starting for histogram-based GBM
#### Description
This is a feature request to add the warm start parameter, which exists for [gradient boosting](https://scikit-learn.org/dev/modules/generated/sklearn.ensemble.GradientBoostingClassifier.html#sklearn.ensemble.GradientBoostingClassifier), to the new [histogram-based gradient boosting](https://scikit-learn.org/dev/modules/generated/sklearn.ensemble.GradientBoostingClassifier.html#sklearn.ensemble.GradientBoostingClassifier).

Rationale: We're using gradient boosting in [Auto-sklearn](https://automl.github.io/auto-sklearn/master/), and fit each model until either the time (given by the user) is up, or the number of trees is reached. For sufficiently large datasets, it is possible that certain configurations time out. Therefore, if possible, we train models iteratively, and only add more trees if time allows. Since the new GBM implementation is really great (not only faster, but also better default accuracy for the problems I tried) we would like to use it within Auto-sklearn as a drop-in, ideally together with iterative training.

This is on my TODO list, but I don't know yet when I'll start working on this.

If anyone wants to give it a try I'll be happy to provide review and/or guidance.
@mfeurer thanks for the input!
@NicolasHug I think this would be great to prioritize. Shouldn't be too hard, right?
Honestly I'm happy to work on it but I've been flooding with a lot of PRs recently (still awaiting reviews) and I feel it'd be more helpful to the project if I go back in reviewing mode for a little while.

It shouldn't be too hard for someone to pick it up.

If I'm not the one writing it, we only need one more reviewer instead of 2.
@NicolasHug I agree re PRs but I feel this one would be relatively small compared to the rest? I might even be able to review it ;)
Also: this ties in with successive halving nicely!
If not one picks it up within a week I'll get started then :)
I'll work on this one, though will probably need some guidance along the way @NicolasHug.
Cool. A good starting point is to look at how this is implemented in `ensemble/gradient_boosting.py`, and start translating it to the new implementation.
@NicolasHug I just realised that this issues is still beyond my current skill level. Looking forward to seeing and learning from the PR though.
Thanks for letting us know, Matts

I can try to work on this if you did not change your mind @NicolasHug .
Cool go ahead!


diff --git a/knowledge_repo/app/routes/comment.py b/knowledge_repo/app/routes/comment.py
index 988d82840..5fe02bce6 100644
--- a/knowledge_repo/app/routes/comment.py
+++ b/knowledge_repo/app/routes/comment.py
@@ -6,7 +6,7 @@
   - /delete_comment
 """
 import logging
-from flask import request, Blueprint, g
+from flask import request, Blueprint, g, escape

 from .. import permissions
 from ..proxies import db_session, current_user
@@ -43,8 +43,7 @@ def post_comment():
                              .first())
     else:
         comment = Comment(post_id=post.id)
-
-    comment.text = data['text']
+    comment.text = escape(data['text'])
     comment.user_id = current_user.id
     db_session.add(comment)
     db_session.commit()

XSS vulnerability
Auto-reviewers: @NiharikaRay @matthewwardrop @earthmancash @danfrankj

Hello, guys!

There is a cross-site scripting (XSS) vulnerability in the Knowledge Repo 0.7.4 (other versions may be affected as well) which allows remote attackers to inject arbitrary JavaScript via post comments functionality.

Steps to reproduce:
Just open any post like this https://site.com/post/posts/new_report.kp
and add the following code as a comment/PoC:
<script>alert("Client side attacks is possible here!");</script>

Impact: An unauthenticated evil user can leverage the vulnerability to conduct various types of client side attacks, for example, conducting a browser mining, redirecting users to malicious sites or hijacking the user’s browser using malware.

As an irrefutable evidence please take a look at the attached screen-shot.

Mitigation: Details on how to prevent XSS can be found here: https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet

PoC:
![xss](https://user-images.githubusercontent.com/6551435/41200588-f832ab3c-6caf-11e8-9f4f-117d153a5473.png)
XSS vulnerabilities in comments marked as safe
Since all comments are marked as safe in `markdown-rendered.html`, someone could write Javascript as a comment and it gets interpreted as raw JS, opening up an XSS vulnerability. It seems like this is done so comments can be written in markdown, possibly for LaTeX as well (though removing `|safe` still seems to render LaTeX correctly). Do you consider the XSS vulnerability a problem? Would you accept a PR to fix it? If so, I was thinking that script tags could be stripped at time of submission, prior to posting to the database. You could also potentially strip all HTML period. It's not clear whether raw HTML should be necessary for any of the supported comment types.

Thank you!

Auto-reviewers: @NiharikaRay @matthewwardrop @earthmancash ancash @danfrankj

Thanks Ekzorcist. I'll make sure this gets fixed before the next release :).
XSS is also present on 0.7.6
Is this one fixed @fahrishb?
Also raised in #254. We should prioritize fixing this.
Hi @redyaffle,

I suspect that this is just one in a myriad of security vulnerabilities that exist throughout the knowledge repo codebase. In the past, this was not considered to be a particularly serious problem, since we internally used the knowledge repo in a trusted computing environment, but as it grows and matures, this sort of thing will be a big deal. I haven't yet personally reviewed much of the code pertaining to comments, so I cannot say exactly why we mark comment content as safe, but my guess is that as you say we render the content server-side before displaying it. It seems, therefore, reasonable to think about adding some functionality to strip unwanted tags/etc from the comment pre-rendering, and if you want to submit a patch that does this, it will be more than welcome!

Many things for getting involved!


diff --git a/CHANGES/3225.misc b/CHANGES/3225.misc
new file mode 100644
index 000000000..e1a600d91
--- /dev/null
+++ b/CHANGES/3225.misc
@@ -0,0 +1,1 @@
+Load one Artifact checksum type taken package_checksum_type during publish metadata generation.
diff --git a/CHANGES/3226.misc b/CHANGES/3226.misc
new file mode 100644
index 000000000..c43519e51
--- /dev/null
+++ b/CHANGES/3226.misc
@@ -0,0 +1,1 @@
+ContentArtifact query performance impovement. Use values() instead of select_related().
diff --git a/pulp_rpm/app/tasks/publishing.py b/pulp_rpm/app/tasks/publishing.py
index 862d95a6c..34c1c00d2 100644
--- a/pulp_rpm/app/tasks/publishing.py
+++ b/pulp_rpm/app/tasks/publishing.py
@@ -123,10 +123,8 @@ def publish_artifacts(self, content, prefix=""):
         published_artifacts = []

         # Special case for Packages
-        contentartifact_qs = (
-            ContentArtifact.objects.filter(content__in=content)
-            .filter(content__pulp_type=Package.get_pulp_type())
-            .select_related("content__rpm_package__time_build")
+        contentartifact_qs = ContentArtifact.objects.filter(content__in=content).filter(
+            content__pulp_type=Package.get_pulp_type()
         )

         rel_path_mapping = defaultdict(list)
@@ -470,8 +468,6 @@ def generate_repo_metadata(
         fil_db = cr.FilelistsSqlite(fil_db_path)
         oth_db = cr.OtherSqlite(oth_db_path)

-    packages = Package.objects.filter(pk__in=content)
-
     # We want to support publishing with a different checksum type than the one built-in to the
     # package itself, so we need to get the correct checksums somehow if there is an override.
     # We must also take into consideration that if the package has not been downloaded the only
@@ -482,45 +478,47 @@ def generate_repo_metadata(
     # simple foreign keys and avoid messing with the many-to-many relationship, which doesn't
     # work with select_related() and performs poorly with prefetch_related(). This is fine
     # because we know that Packages should only ever have one artifact per content.
-    contentartifact_qs = (
-        ContentArtifact.objects.filter(content__in=packages.only("pk"))
-        .select_related(
-            # content__rpm_package is a bit of a hack, exploiting the way django sets up model
-            # inheritance, but it works and is unlikely to break. All content artifacts being
-            # accessed here have an associated Package since they originally came from the
-            # Package queryset.
-            "artifact",
-            "content__rpm_package",
-        )
-        .only("artifact", "content__rpm_package__checksum_type", "content__rpm_package__pkgId")
-    )
+    fields = [
+        "content_id",
+        "content__rpm_package__checksum_type",
+        "content__rpm_package__pkgId",
+    ]
+    artifact_checksum = None
+    if package_checksum_type:
+        package_checksum_type = package_checksum_type.lower()
+        artifact_checksum = f"artifact__{package_checksum_type}"
+        fields.append(artifact_checksum)
+
+    contentartifact_qs = ContentArtifact.objects.filter(
+        content__in=content, content__pulp_type=Package.get_pulp_type()
+    ).values(*fields)

     pkg_to_hash = {}
     for ca in contentartifact_qs.iterator():
         if package_checksum_type:
-            package_checksum_type = package_checksum_type.lower()
-            pkgid = getattr(ca.artifact, package_checksum_type, None)
+            pkgid = ca.get(artifact_checksum, None)

         if not package_checksum_type or not pkgid:
-            if ca.content.rpm_package.checksum_type not in settings.ALLOWED_CONTENT_CHECKSUMS:
+            if ca["content__rpm_package__checksum_type"] not in settings.ALLOWED_CONTENT_CHECKSUMS:
                 raise ValueError(
-                    "Package {} as content unit {} contains forbidden checksum type '{}', "
-                    "thus can't be published. {}".format(
-                        ca.content.rpm_package.nevra,
-                        ca.content.pk,
-                        ca.content.rpm_package.checksum_type,
+                    "Package with pkgId {} as content unit {} contains forbidden checksum type "
+                    "'{}', thus can't be published. {}".format(
+                        ca["content__rpm_package__pkgId"],
+                        ca["content_id"],
+                        ca["content__rpm_package__checksum_type"],
                         ALLOWED_CHECKSUM_ERROR_MSG,
                     )
                 )
-            package_checksum_type = ca.content.rpm_package.checksum_type
-            pkgid = ca.content.rpm_package.pkgId
+            package_checksum_type = ca["content__rpm_package__checksum_type"]
+            pkgid = ca["content__rpm_package__pkgId"]

-        pkg_to_hash[ca.content_id] = (package_checksum_type, pkgid)
+        pkg_to_hash[ca["content_id"]] = (package_checksum_type, pkgid)

     # TODO: this is meant to be a !! *temporary* !! fix for
     # https://github.com/pulp/pulp_rpm/issues/2407
     pkg_pks_to_ignore = set()
     latest_build_time_by_nevra = defaultdict(list)
+    packages = Package.objects.filter(pk__in=content)
     for pkg in packages.only(
         "pk", "name", "epoch", "version", "release", "arch", "time_build"
     ).iterator():
diff --git a/pulp_rpm/app/tasks/synchronizing.py b/pulp_rpm/app/tasks/synchronizing.py
index 2b170add9..aeebe24b6 100644
--- a/pulp_rpm/app/tasks/synchronizing.py
+++ b/pulp_rpm/app/tasks/synchronizing.py
@@ -174,19 +174,15 @@ def add_metadata_to_publication(publication, version, prefix=""):
     published_artifacts = []

     # Handle packages
-    pkg_data = (
-        ContentArtifact.objects.filter(
-            content__in=version.content, content__pulp_type=Package.get_pulp_type()
-        )
-        .select_related("content__rpm_package")
-        .only("pk", "artifact", "content", "content__rpm_package__pkgId")
-    )
+    pkg_data = ContentArtifact.objects.filter(
+        content__in=version.content, content__pulp_type=Package.get_pulp_type()
+    ).values("pk", "content__rpm_package__pkgId")
     for ca in pkg_data.iterator():
         for relative_path in pkgid_to_location_href[str(version.repository.pk)][
-            ca.content.rpm_package.pkgId
+            ca["content__rpm_package__pkgId"]
         ]:
             pa = PublishedArtifact(
-                content_artifact=ca,
+                content_artifact_id=ca["pk"],
                 relative_path=os.path.join(prefix, relative_path),
                 publication=publication,
             )

Switch to values() instead of select_related
**Version**
all
**Describe the bug**
Some queries use select_related, if we do not need the model instance, we could just use values() that will produce queryset dict results instead.

**To Reproduce**
Steps to reproduce the behavior:

**Expected behavior**
A clear and concise description of what you expected to happen.

**Additional context**
Add any other context about the problem here.  Please provide links to any previous discussions via Discourse or Bugzilla.

During publish metadata generation load just one Artifact checksum type
**Version**
all

**Describe the bug**
There is an overhead which consists in loading all artifact checksum types during publish repodata generation. It's enough to load just one, the one specified in package_checksum_type

**To Reproduce**
Steps to reproduce the behavior:

**Expected behavior**
A clear and concise description of what you expected to happen.

**Additional context**
Add any other context about the problem here.  Please provide links to any previous discussions via Discourse or Bugzilla.


diff --git a/src/drivers/linker.py b/src/drivers/linker.py
index 4f3ec85..c369e43 100644
--- a/src/drivers/linker.py
+++ b/src/drivers/linker.py
@@ -17,6 +17,7 @@

 MAC_SIZE = int(sancus.config.SECURITY / 8)
 KEY_SIZE = MAC_SIZE
+CONNECTION_STRUCT_SIZE = 6 + KEY_SIZE

 class SmEntry:
@@ -63,7 +64,7 @@ def add_sym(file, sym_map):

     args += [file, file]
     call_prog('msp430-elf-objcopy', args)
-    return file
+    return file

 def parse_size(val):
@@ -92,18 +93,23 @@ def get_symbol(elf_file, name):

 def get_io_sym_map(sm_name):
     sym_map = {
-        '__sm_handle_input':    '__sm_{}_handle_input'.format(sm_name),
-        '__sm_num_inputs':      '__sm_{}_num_inputs'.format(sm_name),
-        '__sm_num_connections': '__sm_{}_num_connections'.format(sm_name),
-        '__sm_io_keys':         '__sm_{}_io_keys'.format(sm_name),
-        '__sm_input_callbacks': '__sm_{}_input_callbacks'.format(sm_name),
-        '__sm_output_nonce':    '__sm_{}_output_nonce'.format(sm_name),
-        '__sm_send_output':     '__sm_{}_send_output'.format(sm_name),
-        '__sm_set_key':         '__sm_{}_set_key'.format(sm_name),
-        '__sm_X_exit':          '__sm_{}_exit'.format(sm_name),
-        '__sm_X_stub_malloc':   '__sm_{}_stub_malloc'.format(sm_name),
+        '__sm_handle_input':        '__sm_{}_handle_input'.format(sm_name),
+        '__sm_num_inputs':          '__sm_{}_num_inputs'.format(sm_name),
+        '__sm_num_connections':     '__sm_{}_num_connections'.format(sm_name),
+        '__sm_max_connections':     '__sm_{}_max_connections'.format(sm_name),
+        '__sm_io_connections':      '__sm_{}_io_connections'.format(sm_name),
+        '__sm_input_callbacks':     '__sm_{}_input_callbacks'.format(sm_name),
+        '__sm_send_output':         '__sm_{}_send_output'.format(sm_name),
+        '__sm_set_key':             '__sm_{}_set_key'.format(sm_name),
+        '__sm_attest':              '__sm_{}_attest'.format(sm_name),
+        '__sm_X_exit':              '__sm_{}_exit'.format(sm_name),
+        '__sm_X_stub_malloc':       '__sm_{}_stub_malloc'.format(sm_name),
         '__sm_X_stub_reactive_handle_output':
-            '__sm_{}_stub_reactive_handle_output'.format(sm_name)
+            '__sm_{}_stub_reactive_handle_output'.format(sm_name),
+        '__sm_X_public_start':      '__sm_{}_public_start'.format(sm_name),
+        '__sm_X_public_end':        '__sm_{}_public_end'.format(sm_name),
+        '__sm_X_secret_start':      '__sm_{}_secret_start'.format(sm_name),
+        '__sm_X_secret_end':        '__sm_{}_secret_end'.format(sm_name)
     }

     return sym_map
@@ -115,7 +121,7 @@ def get_io_sect_map(sm_name):
         '.rela.sm.X.text':  '.rela.sm.{}.text'.format(sm_name),
     }

-    for entry in ('__sm{}_set_key', '__sm{}_handle_input'):
+    for entry in ('__sm{}_set_key', '__sm{}_attest', '__sm{}_handle_input'):
         map['.sm.X.{}.table'.format(entry.format(''))] = \
             '.sm.{}.{}.table'.format(sm_name, entry.format('_' + sm_name))
         map['.rela.sm.X.{}.table'.format(entry.format(''))] = \
@@ -136,15 +142,19 @@ def create_io_stub(sm, stub):

 def sort_entries(entries):
-    # If the set_key entry exists, it should have index 0 and if the
-    # handle_input entry exists, it should have index 1. This is accomplished by
-    # mapping those entries to __ and ___ respectively since those come
+    # If the set_key entry exists, it should have index 0, if the
+    # attest entry exists, it should have index 1 and if
+    # handle_input entry exists, it should have index 2. This is accomplished by
+    # mapping those entries to __, ___ and ___ respectively since those come
     # alphabetically before any valid entry name.
     def sort_key(entry):
         if re.match(r'__sm_\w+_set_key', entry.name):
             return '__'
-        if re.match(r'__sm_\w+_handle_input', entry.name):
+        if re.match(r'__sm_\w+_attest', entry.name):
             return '___'
+        if re.match(r'__sm_\w+_handle_input', entry.name):
+            return '____'
+
         return entry.name

     entries.sort(key=sort_key)
@@ -212,7 +222,7 @@ def sort_key(entry):
     for a in archive_files:
         debug("Unpacking archive for Sancus SM inspection: " + a)
         file_name = a
-        if ':' in a:
+        if ':' in a:
             # support calls such as -lib:/full/path
             file_name = file_name.split(':')[1]

@@ -269,6 +279,7 @@ def sort_key(entry):
 elf_relocations = defaultdict(list)

 added_set_key_stub = False
+added_attest_stub = False
 added_input_stub = False
 added_output_stub = False

@@ -409,6 +420,14 @@ def sort_key(entry):
                         input_files_to_scan.append(generated_file)
                         added_set_key_stub = True

+                    if not added_attest_stub:
+                        # Generate the attest stub file
+                        generated_file = create_io_stub(sm, 'sm_attest.o')
+                        generated_object_files.append(generated_file)
+                        # And register it to also be scanned by this loop later
+                        input_files_to_scan.append(generated_file)
+                        added_attest_stub = True
+
                     if which == 'input':
                         dest = sms_inputs

@@ -463,7 +482,7 @@ def sort_key(entry):
 The YAML file allows to set the following things:
   1) warn whenever an ocall is performed and abort linking process
   2) Swap out assembly stubs for custom project dependent values
-  3) Set a peripheral offset for the first SM to
+  3) Set a peripheral offset for the first SM to
 """
 try:
     file_path = ''
@@ -829,24 +848,36 @@ def sort_key(entry):

         call_prog('msp430-gcc', ['-c', '-o', o_file, c_file])

-        input_callbacks += '    {}(.sm.{}.callbacks)\n'.format(o_file, sm)
+        input_callbacks += '    KEEP({}(.sm.{}.callbacks))\n'.format(o_file, sm)
         input_callbacks += '    . = ALIGN(2);'

-    # Table of connection keys
-    io_keys = ''
+    """
+    Table of connections: in a reactive application, a connection links the
+    output of a SM (defined using the macro `SM_OUTPUT`) to the input of another
+    (defined using the macro `SM_INPUT`).
+    These connections are stored in a `Connection` array on each SM (see
+    `reactive_stubs_support.h`). The array is allocated here with a fixed size,
+    according to the `num_connections` parameter in the SM config (default 0).
+    """

-    if len(ios) > 0:
-        io_keys += '__sm_{}_io_keys = .;\n'.format(sm)
-        io_keys += '    . += {};\n'.format(len(ios) * KEY_SIZE)
-        io_keys += '    . = ALIGN(2);'
+    num_connections = ''
+    io_connections = ''
+
+    if hasattr(sm_config[sm], "num_connections"):
+        sm_num_connections = sm_config[sm].num_connections
+    else:
+        sm_num_connections = 0

-    # Nonce used by outputs
-    outputs_nonce = ''
+    if len(ios) > 0:
+        # make sure we allocate space even if num_connections is zero
+        io_connections_size = max(sm_num_connections * CONNECTION_STRUCT_SIZE, 2)

-    if len(outputs) > 0:
-        outputs_nonce += '__sm_{}_output_nonce = .;\n'.format(sm)
-        outputs_nonce += '    . += 2;\n'
-        outputs_nonce += '    . = ALIGN(2);'
+        num_connections += '__sm_{}_num_connections = .;\n'.format(sm)
+        num_connections += '    . += 2;\n'
+        num_connections += '    . = ALIGN(2);'
+        io_connections += '__sm_{}_io_connections = .;\n'.format(sm)
+        io_connections += '    . += {};\n'.format(io_connections_size)
+        io_connections += '    . = ALIGN(2);'

     # Set data section offset if peripheral access is set
     # in that case, optionally provide first SM with exclusive access to last peripheral
@@ -859,10 +890,10 @@ def sort_key(entry):
                                              exit_file, '\n    '.join(tables),
                                              input_callbacks,
                                              '\n    '.join(extra_labels)))
-
+
     data_sections.append(data_section.format(sm, '\n    '.join(id_syms),
-                                             args.sm_stack_size, io_keys,
-                                             outputs_nonce,
+                                             args.sm_stack_size, num_connections,
+                                             io_connections,
                                              data_section_start))

     if sm in sms_entries:
@@ -880,7 +911,7 @@ def sort_key(entry):
         symbols.append('__sm_{}_io_{}_idx = {};'.format(sm, io, index))

     # Add symbols for the number of connections/inputs
-    symbols.append('__sm_{}_num_connections = {};'.format(sm, len(ios)))
+    symbols.append('__sm_{}_max_connections = {};'.format(sm, sm_num_connections))
     symbols.append('__sm_{}_num_inputs = {};'.format(sm, len(inputs)))

     if args.prepare_for_sm_text_section_wrapping:
diff --git a/src/drivers/sancus/sancus_config.py b/src/drivers/sancus/sancus_config.py
index c9826e1..b29bf0c 100644
--- a/src/drivers/sancus/sancus_config.py
+++ b/src/drivers/sancus/sancus_config.py
@@ -12,6 +12,7 @@
 SM_CONFIG_DEFAULT_SM_EXIT=""
 SM_CONFIG_DEFAULT_SM_ISR=""
 SM_CONFIG_DEFAULT_PERIPHERAL_OFFSET=0
+SM_CONFIG_DEFAULT_NUM_CONNECTIONS=None

 class SmParserError(Exception):
     """
@@ -125,6 +126,7 @@ def _safe_config_parse(name, default_val):
         self._sm_exit  = _safe_config_parse('sm_exit',  SM_CONFIG_DEFAULT_SM_EXIT)
         self._sm_isr   = _safe_config_parse('sm_isr' ,  SM_CONFIG_DEFAULT_SM_ISR)
         self._peripheral_offset = _safe_config_parse('peripheral_offset', 0)
+        self._num_connections = _safe_config_parse('num_connections', SM_CONFIG_DEFAULT_NUM_CONNECTIONS)

     """
     Getters as properties for all class variables.
@@ -178,6 +180,13 @@ def peripheral_offset(self):
         else:
             raise AttributeError

+    @property
+    def num_connections(self):
+        if self._num_connections != SM_CONFIG_DEFAULT_NUM_CONNECTIONS:
+            return abs(self._num_connections)
+        else:
+            raise AttributeError
+
     def __str__(self):
         if self._contains_config:
             s = '' + self._name + ':\n'
@@ -187,6 +196,7 @@ def __str__(self):
             if hasattr(self, 'sm_exit'): s += '\t\t SM_EXIT: ' + self._sm_exit  + '\n'
             if hasattr(self, 'sm_isr'): s += '\t\t SM_ISR: ' + self._sm_isr  + '\n'
             if hasattr(self, 'peripheral_offset'): s += '\t\t Peripheral Offset: ' + str(self._peripheral_offset)  + '\n'
+            if hasattr(self, 'num_connections'): s += '\t\t Number of connections: ' + str(self._num_connections)  + '\n'
         else:
             s = 'No YAML config provided.'
         return s
\ No newline at end of file
diff --git a/src/drivers/sm-config-example.yaml b/src/drivers/sm-config-example.yaml
index 4c82eed..b2a8d13 100644
--- a/src/drivers/sm-config-example.yaml
+++ b/src/drivers/sm-config-example.yaml
@@ -23,6 +23,13 @@ my_sm_1:
     # amount of byte. This allows to map the data section over the
     # peripherals to grant exclusive and faster access to these peripherals
     - peripheral_offset: 159
+    # Set the maximum number of connections allowed for this SM in a reactive
+    # application. Connections link the output of a SM (defined using the macro
+    # `SM_OUTPUT`) with the input of another (defined using the macro
+    # `SM_INPUT`). If this SM does not have any inputs nor outputs, there is no
+    # need to specify this parameter (default value is zero). In the Authentic
+    # Execution framework, this parameter is automatically configured.
+    - num_connections: 3
 my_sm_2:
  	- disallow_outcalls: True

\ No newline at end of file
diff --git a/src/sancus_support/reactive.h b/src/sancus_support/reactive.h
index db60b61..d3bebc5 100644
--- a/src/sancus_support/reactive.h
+++ b/src/sancus_support/reactive.h
@@ -6,19 +6,20 @@
 #include <stdint.h>

 typedef uint16_t io_index;
+typedef uint16_t conn_index;
 typedef uint8_t  io_data __attribute__((aligned(2)));

 // The ASM symbols are used for the linker to be able to detect inputs/outputs

-#define SM_OUTPUT_AUX(sm, name)                                         \
-    asm("__sm_" #sm "_output_tag_" #name " = 0\n");                     \
-    SM_FUNC(sm) void name(const io_data* data, size_t len)              \
-    {                                                                   \
-        extern char __sm_##sm##_io_##name##_idx;                        \
-        SM_FUNC(sm) void __sm_##sm##_send_output(unsigned int,          \
-                                                 const void*, size_t);  \
-        __sm_##sm##_send_output((io_index)&__sm_##sm##_io_##name##_idx, \
-                                data, len);                             \
+#define SM_OUTPUT_AUX(sm, name)                                                \
+    asm("__sm_" #sm "_output_tag_" #name " = 0\n");                            \
+    SM_FUNC(sm) uint16_t name(const io_data* data, size_t len)                 \
+    {                                                                          \
+        extern char __sm_##sm##_io_##name##_idx;                               \
+        SM_FUNC(sm) uint16_t __sm_##sm##_send_output(unsigned int,             \
+                                                 const void*, size_t);         \
+        return __sm_##sm##_send_output((io_index)&__sm_##sm##_io_##name##_idx, \
+                                data, len);                                    \
     }

 #define SM_OUTPUT(sm, name) SM_OUTPUT_AUX(sm, name)
diff --git a/src/sancus_support/sm_support.h b/src/sancus_support/sm_support.h
index 9881973..12264ef 100644
--- a/src/sancus_support/sm_support.h
+++ b/src/sancus_support/sm_support.h
@@ -4,6 +4,7 @@
 #include "config.h"

 #include <stddef.h>
+#include <stdint.h>

 #if __GNUC__ >= 5 || __clang_major__ >= 5

@@ -199,15 +200,12 @@ extern char __unprotected_sp;

 #endif

-#define __OUTSIDE_SM( p, sm )                                                  \
-    ( ((void*) p < (void*) &__PS(sm)) || ((void*) p >= (void*) &__PE(sm)) ) && \
-    ( ((void*) p < (void*) &__SS(sm)) || ((void*) p >= (void*) &__SE(sm)) )
-
 /*
  * Returns true iff whole buffer [p,p+len-1] is outside of the sm SancusModule
  */
-#define sancus_is_outside_sm( sm, p, len) \
-    ( __OUTSIDE_SM(p, sm) && __OUTSIDE_SM((p+len-1), sm) )
+#define sancus_is_outside_sm(sm, p, len) \
+    ( is_buffer_outside_region(&__PS(sm), &__PE(sm), p, len) && \
+      is_buffer_outside_region(&__SS(sm), &__SE(sm), p, len) )

 /**
  * Interrupt vector for the Sancus violation ISR.
@@ -311,6 +309,37 @@ sm_id sancus_enable_wrapped(struct SancusModule* sm, unsigned nonce, void* tag);
 #undef always_inline
 #define always_inline static inline __attribute__((always_inline))

+/*
+ * Returns true if buf is outside the memory region [start, end)
+ * if start >= end, immediately return false
+ */
+always_inline int is_buffer_outside_region(void *start_p, void *end_p,
+    void *buf_p, size_t len) {
+  uintptr_t start = (uintptr_t) start_p;
+  uintptr_t end   = (uintptr_t) end_p;
+  uintptr_t buf   = (uintptr_t) buf_p;
+  uintptr_t buf_end;
+
+  // make sure start < end, otherwise return false
+  if (start >= end) {
+    return 0;
+  }
+
+  if(len > 0) {
+    buf_end = buf + len - 1;
+  }
+  else {
+    buf_end = buf;
+  }
+
+  /* check for int overflow and finally validate `buf` falls outside */
+  if( (buf <= buf_end) && ((end <= buf) || (start > buf_end))) {
+    return 1;
+  }
+
+  return 0;
+}
+
 /**
  * Performs constant time comparison between to buffers
  * Returns 0 if the first `n` bytes of the two buffers are equal, -1 otherwise
diff --git a/src/stubs/CMakeLists.txt b/src/stubs/CMakeLists.txt
index 9427a17..9172a8e 100644
--- a/src/stubs/CMakeLists.txt
+++ b/src/stubs/CMakeLists.txt
@@ -23,6 +23,7 @@ set(EXTRA_FLAGS -I${CMAKE_SOURCE_DIR}/src/sancus_support)
 add_object(sm_output.o sm_output.c ${EXTRA_FLAGS})
 add_object(sm_input.o sm_input.c ${EXTRA_FLAGS})
 add_object(sm_set_key.o sm_set_key.c ${EXTRA_FLAGS})
+add_object(sm_attest.o sm_attest.c ${EXTRA_FLAGS})

 set(STUBS
     ${CMAKE_CURRENT_BINARY_DIR}/sm_entry.o
@@ -34,6 +35,7 @@ set(STUBS
     ${CMAKE_CURRENT_BINARY_DIR}/sm_output.o
     ${CMAKE_CURRENT_BINARY_DIR}/sm_input.o
     ${CMAKE_CURRENT_BINARY_DIR}/sm_set_key.o
+    ${CMAKE_CURRENT_BINARY_DIR}/sm_attest.o
     ${CMAKE_CURRENT_BINARY_DIR}/sm_mmio_entry.o
     ${CMAKE_CURRENT_BINARY_DIR}/sm_mmio_exclusive.o

diff --git a/src/stubs/reactive_stubs_support.h b/src/stubs/reactive_stubs_support.h
index c5aca41..3c7a251 100644
--- a/src/stubs/reactive_stubs_support.h
+++ b/src/stubs/reactive_stubs_support.h
@@ -3,29 +3,54 @@

 #include "reactive.h"

-void reactive_handle_output(io_index output_id, void* data, size_t len);
+void reactive_handle_output(conn_index conn_id, void* data, size_t len);

 typedef uint8_t IoKey[SANCUS_KEY_SIZE];
 typedef void (*InputCallback)(const void*, size_t);

 typedef enum
 {
-    Ok                = 0x0,
-    IllegalConnection = 0x1,
-    MalformedPayload  = 0x2
+    Ok                    = 0x0,
+    IllegalConnection     = 0x1,
+    MalformedPayload      = 0x2,
+    IllegalParameters     = 0x3,
+    BufferInsideSM        = 0x4,
+    CryptoError           = 0x5,
+    InternalError         = 0x6
 } ResultCode;

+typedef struct Connection {
+    io_index    io_id;
+    conn_index  conn_id;
+    uint16_t    nonce;
+    IoKey       key;
+} Connection;
+
+// The size of the Connection struct is also hardcoded in linker.py. Hence,
+// we need to make sure that it does not change at compile time (e.g. due to
+// optimizations).
+// Besides, if the struct changes, we need to adjust this value here and in
+// linker.py (check the CONNECTION_STRUCT_SIZE global variable) as well.
+_Static_assert (sizeof(Connection) == 6 + SANCUS_KEY_SIZE,
+    "Size of Connection struct differs from the expected value");
+
 // These will be allocated by the linker
-extern IoKey __sm_io_keys[];
+extern Connection __sm_io_connections[];
 extern InputCallback __sm_input_callbacks[];
-extern uint16_t __sm_output_nonce;

-extern char __sm_num_connections;
-#define SM_NUM_CONNECTIONS (size_t)&__sm_num_connections
+extern char __sm_max_connections;
+#define SM_MAX_CONNECTIONS (size_t)&__sm_max_connections
+
+extern uint16_t __sm_num_connections;

 extern char __sm_num_inputs;
 #define SM_NUM_INPUTS (size_t)&__sm_num_inputs

 #define SM_NAME X

+// declare symbols for the public/secret regions
+#define __SECTION(sect, name) sect(name)
+extern char __SECTION(__PS, SM_NAME), __SECTION(__PE, SM_NAME),
+            __SECTION(__SS, SM_NAME), __SECTION(__SE, SM_NAME);
+
 #endif
diff --git a/src/stubs/sm_input.c b/src/stubs/sm_input.c
index 992d2e6..05f3f55 100644
--- a/src/stubs/sm_input.c
+++ b/src/stubs/sm_input.c
@@ -2,24 +2,42 @@

 #include <alloca.h>

-#define AD_SIZE 2
-
-void SM_ENTRY(SM_NAME) __sm_handle_input(uint16_t conn_id,
+uint16_t SM_ENTRY(SM_NAME) __sm_handle_input(uint16_t conn_idx,
                                          const void* payload, size_t len)
 {
-    if (conn_id >= SM_NUM_INPUTS)
-        return;
+    // sanitize input buffer
+    if(!sancus_is_outside_sm(SM_NAME, (void *) payload, len)) {
+      return BufferInsideSM;
+    }

-    const size_t data_len = len - AD_SIZE - SANCUS_TAG_SIZE;
-    const uint8_t* cipher = (uint8_t*)payload + AD_SIZE;
-    const uint8_t* tag = cipher + data_len;
+    // check correctness of other parameters
+    if(len < SANCUS_TAG_SIZE || conn_idx >= __sm_num_connections) {
+      return IllegalParameters;
+    }
+
+    Connection *conn = &__sm_io_connections[conn_idx];

+    // check if io_id is a valid input ID
+    if (conn->io_id >= SM_NUM_INPUTS) {
+      return IllegalConnection;
+    }
+
+    // associated data only contains the nonce, therefore we can use this
+    // this trick to build the array fastly (i.e. by swapping the bytes)
+    const uint16_t nonce_rev = conn->nonce << 8 | conn->nonce >> 8;
+    const size_t data_len = len - SANCUS_TAG_SIZE;
+    const uint8_t* cipher = payload;
+    const uint8_t* tag = cipher + data_len;
     // TODO check for stack overflow!
     uint8_t* input_buffer = alloca(data_len);

-    if (sancus_unwrap_with_key(__sm_io_keys[conn_id], payload, AD_SIZE,
-                               cipher, data_len, tag, input_buffer))
-    {
-        __sm_input_callbacks[conn_id](input_buffer, data_len);
+    if (sancus_unwrap_with_key(conn->key, &nonce_rev, sizeof(nonce_rev),
+                               cipher, data_len, tag, input_buffer)) {
+       conn->nonce++;
+       __sm_input_callbacks[conn->io_id](input_buffer, data_len);
+       return Ok;
     }
+
+    // here only if decryption fails
+    return CryptoError;
 }
diff --git a/src/stubs/sm_output.c b/src/stubs/sm_output.c
index 6febf29..cc71424 100644
--- a/src/stubs/sm_output.c
+++ b/src/stubs/sm_output.c
@@ -2,14 +2,38 @@

 #include <stdlib.h>

-SM_FUNC(SM_NAME) void __sm_send_output(io_index index,
+SM_FUNC(SM_NAME) uint16_t __sm_send_output(io_index index,
                                        const void* data, size_t len)
 {
-    const size_t nonce_len = sizeof(__sm_output_nonce);
-    const size_t payload_len = nonce_len + len + SANCUS_TAG_SIZE;
-    uint8_t* payload = malloc(payload_len);
-    *(uint16_t*)payload = __sm_output_nonce++;
-    sancus_wrap_with_key(__sm_io_keys[index], payload, nonce_len, data, len,
-                         payload + nonce_len, payload + nonce_len + len);
-    reactive_handle_output(index, payload, payload_len);
+    const size_t payload_len = len + SANCUS_TAG_SIZE;
+
+    // search for all the connections associated to the index.
+    // Unfortunately, this operation is O(n) with n = number of connections
+    int i;
+    for (i=0; i<__sm_num_connections; i++) {
+      Connection *conn = &__sm_io_connections[i];
+      if (conn->io_id != index) {
+          continue;
+      }
+
+      uint8_t* payload = malloc(payload_len);
+
+      if (payload == NULL) {
+          return InternalError;
+      }
+
+      if ( !sancus_is_outside_sm(SM_NAME, (void *) payload, payload_len) ) {
+          return BufferInsideSM;
+      }
+
+      // associated data only contains the nonce, therefore we can use this
+      // this trick to build the array fastly (i.e. by swapping the bytes)
+      uint16_t nonce_rev = conn->nonce << 8 | conn->nonce >> 8;
+      sancus_wrap_with_key(conn->key, &nonce_rev, sizeof(nonce_rev), data,
+                          len, payload, payload + len);
+      conn->nonce++;
+      reactive_handle_output(conn->conn_id, payload, payload_len);
+    }
+
+    return Ok;
 }
diff --git a/src/stubs/sm_set_key.c b/src/stubs/sm_set_key.c
index e982d4c..7694958 100644
--- a/src/stubs/sm_set_key.c
+++ b/src/stubs/sm_set_key.c
@@ -1,21 +1,43 @@
 #include "reactive_stubs_support.h"

-void SM_ENTRY(SM_NAME) __sm_set_key(const uint8_t* ad, const uint8_t* cipher,
-                                    const uint8_t* tag, uint8_t* result)
+#define AD_SIZE 6
+
+uint16_t SM_ENTRY(SM_NAME) __sm_set_key(const uint8_t* ad, const uint8_t* cipher,
+                                    const uint8_t* tag, uint16_t *conn_idx)
 {
-    uint16_t conn_id = (ad[2] << 8) | ad[3];
-    ResultCode code = Ok;
-
-    if (conn_id >= SM_NUM_CONNECTIONS)
-        code = IllegalConnection;
-    else if (!sancus_unwrap(ad, 4, cipher, SANCUS_KEY_SIZE, tag,
-             __sm_io_keys[conn_id]))
-    {
-        code = MalformedPayload;
+    if( !sancus_is_outside_sm(SM_NAME, (void *) ad, AD_SIZE) ||
+        !sancus_is_outside_sm(SM_NAME, (void *) cipher, SANCUS_KEY_SIZE) ||
+        !sancus_is_outside_sm(SM_NAME, (void *) tag, SANCUS_TAG_SIZE) ||
+        !sancus_is_outside_sm(SM_NAME, (void *) conn_idx, sizeof(uint16_t)) ) {
+      return BufferInsideSM;
+    }
+
+    // Note: make sure we only use AD_SIZE bytes of the buffer `ad`
+    conn_index conn_id = (ad[0] << 8) | ad[1];
+    io_index io_id = (ad[2] << 8) | ad[3];
+    uint16_t nonce = (ad[4] << 8) | ad[5];
+
+    // check if there is still space left in the array
+    if (__sm_num_connections == SM_MAX_CONNECTIONS) {
+      return InternalError;
+    }
+
+    // check nonce
+    if(nonce != __sm_num_connections) {
+      return MalformedPayload;
     }

-    result[0] = 0;
-    result[1] = code;
-    uint8_t result_ad[] = {ad[0], ad[1], result[0], result[1]};
-    sancus_tag(result_ad, sizeof(result_ad), result + 2);
+    Connection *conn = &__sm_io_connections[__sm_num_connections];
+    *conn_idx = __sm_num_connections;
+
+    if (!sancus_unwrap(ad, AD_SIZE, cipher, SANCUS_KEY_SIZE, tag, conn->key)) {
+      return CryptoError;
+    }
+
+    __sm_num_connections++;
+    conn->io_id = io_id;
+    conn->conn_id = conn_id;
+    conn->nonce = 0;
+
+    return Ok;
 }

diff --git a/src/stubs/sm_attest.c b/src/stubs/sm_attest.c
new file mode 100644
index 0000000..6bfec25
--- /dev/null
+++ b/src/stubs/sm_attest.c
@@ -0,0 +1,16 @@
+#include "reactive_stubs_support.h"
+
+uint16_t SM_ENTRY(SM_NAME) __sm_attest(const uint8_t* challenge, size_t len,
+  uint8_t *result)
+{
+    if( !sancus_is_outside_sm(SM_NAME, (void *) challenge, len) ||
+        !sancus_is_outside_sm(SM_NAME, (void *) result, SANCUS_TAG_SIZE) ) {
+      return BufferInsideSM;
+    }
+
+    if( !sancus_tag(challenge, len, result) ) {
+        return CryptoError;
+    }
+
+    return Ok;
+}

Check for integer overflow in sancus_is_outside_sm macro

thanks for the PR! Not sure about your first clause "( __OUTSIDE_SM(p, sm) && ((len <= 0) ||". If the length is zero, the condition should always be true and the outside_sm is not necessary I'd say. A negative length doesn't make any sense and the disadvantage of a macro is that we can't easily enforce an `unsigned` type as in an inlined function..

The logical OR is flawed I think. You never check the first condition `__OUTSIDE_SM(p, sm)` in the OR clause? So only the end pointer is checked, which renders the check vulnerable AFAIS?

I feel the logic is getting to complex for a macro and I think this will be better implemented as an always_inline function with typed arguments, making use of the OUTSIDE_SM helper macros?

Maybe a good place to start is the logic from the SGX-SDK:

https://github.com/intel/linux-sgx/blob/4589daddd58bec7367a6a9de3fe301e6de17671a/sdk/trts/trts.cpp#L101

```C
// sgx_is_outside_enclave()
// Parameters:
//      addr - the start address of the buffer
//      size - the size of the buffer
// Return Value:
//      1 - the buffer is strictly outside the enclave
//      0 - the whole buffer or part of the buffer is not outside the enclave,
//          or the buffer is wrap around
//
int sgx_is_outside_enclave(const void *addr, size_t size)
{
    size_t start = reinterpret_cast<size_t>(addr);
    size_t end = 0;
    size_t enclave_start = (size_t)&__ImageBase;
    size_t enclave_end = enclave_start + g_global_data.enclave_size - 1;
    // g_global_data.enclave_end = enclave_base + enclave_size - 1;
    // so the enclave range is [enclave_start, enclave_end] inclusively

    if(size > 0)
    {
        end = start + size - 1;
    }
    else
    {
        end = start;
    }
    if( (start <= end) && ((end < enclave_start) || (start > enclave_end)) )
    {
        return 1;
    }
    return 0;
}
```
Thanks for the comments! I think you might have mentally skipped a bracket, as the start address is always checked, and then there is (size is below or equal to zero) OR (buffer does not wrap around and end address is outside of enclave), which is similar to the SGX logic. Nevertheless, this might be the proof this should be in a function, I'll take care of it!
thanks, yes you're absolutely right I mis-parsed the brackets my bad! :sweat_smile: It's indeed an indication that we'll be better off with an inlined function :)
hm important update--re-reading the Tale of 2 worlds analysis, I remember the problem w this code goes beyond merely integer overflows:

> Sancus. We found both logical errors and integer overflow vulnerabilities
in the sancus_is_outside_sm() function provided by the trusted runtime.
Particularly, the current implementation does not properly detect an untrusted
buffer that spans the entire enclave address range, or a rogue length specifier
that triggers an integer overflow to wrap around the 16-bit address space.

The code needs to be rewritten as in the Intel SGX code stub above. Currently, the code is fine if your buffer starts below the enclave and then (without overflowing) ends after the enclave, overwriting the entire enclave(!)


diff --git a/doc/whats_new.rst b/doc/whats_new.rst
index b5f10da91d28f..a4b775ec66d0a 100644
--- a/doc/whats_new.rst
+++ b/doc/whats_new.rst
@@ -36,6 +36,11 @@ Enhancements
      (`#6288 <https://github.com/scikit-learn/scikit-learn/pull/6288>`_) by
      `Jake VanderPlas`_.

+   - :class:`ensemble.GradientBoostingClassifier` and :class:`ensemble.GradientBoostingRegressor`
+     now support sparse input for prediction.
+     (`#6101 <https://github.com/scikit-learn/scikit-learn/pull/6101>`_)
+     By `Ibraim Ganiev`_.
+
 Bug fixes
 .........

diff --git a/sklearn/ensemble/_gradient_boosting.pyx b/sklearn/ensemble/_gradient_boosting.pyx
index 9e6e9f6d29c0e..71371f5c24a48 100644
--- a/sklearn/ensemble/_gradient_boosting.pyx
+++ b/sklearn/ensemble/_gradient_boosting.pyx
@@ -8,13 +8,22 @@

 cimport cython

+from libc.stdlib cimport free
+from libc.string cimport memset
+
 import numpy as np
 cimport numpy as np
 np.import_array()

+from scipy.sparse import issparse
+from scipy.sparse import csr_matrix
+
 from sklearn.tree._tree cimport Node
 from sklearn.tree._tree cimport Tree
-
+from sklearn.tree._tree cimport DTYPE_t
+from sklearn.tree._tree cimport SIZE_t
+from sklearn.tree._tree cimport INT32_t
+from sklearn.tree._utils cimport safe_realloc

 ctypedef np.int32_t int32
 ctypedef np.float64_t float64
@@ -27,24 +36,19 @@ from numpy import bool as np_bool
 from numpy import float32 as np_float32
 from numpy import float64 as np_float64

-# Define a datatype for the data array
-DTYPE = np.float32
-ctypedef np.float32_t DTYPE_t
-ctypedef np.npy_intp SIZE_t
-

 # constant to mark tree leafs
-cdef int LEAF = -1
-
-cdef void _predict_regression_tree_inplace_fast(DTYPE_t *X,
-                                                Node* root_node,
-                                                double *value,
-                                                double scale,
-                                                Py_ssize_t k,
-                                                Py_ssize_t K,
-                                                Py_ssize_t n_samples,
-                                                Py_ssize_t n_features,
-                                                float64 *out):
+cdef SIZE_t TREE_LEAF = -1
+
+cdef void _predict_regression_tree_inplace_fast_dense(DTYPE_t *X,
+                                                      Node* root_node,
+                                                      double *value,
+                                                      double scale,
+                                                      Py_ssize_t k,
+                                                      Py_ssize_t K,
+                                                      Py_ssize_t n_samples,
+                                                      Py_ssize_t n_features,
+                                                      float64 *out):
     """Predicts output for regression tree and stores it in ``out[i, k]``.

     This function operates directly on the data arrays of the tree
@@ -84,22 +88,108 @@ cdef void _predict_regression_tree_inplace_fast(DTYPE_t *X,
         shape ``(n_samples, K)``.
     """
     cdef Py_ssize_t i
-    cdef int32 node_id
     cdef Node *node
     for i in range(n_samples):
         node = root_node
         # While node not a leaf
-        while node.left_child != -1 and node.right_child != -1:
+        while node.left_child != TREE_LEAF:
             if X[i * n_features + node.feature] <= node.threshold:
                 node = root_node + node.left_child
             else:
                 node = root_node + node.right_child
         out[i * K + k] += scale * value[node - root_node]

+def _predict_regression_tree_stages_sparse(np.ndarray[object, ndim=2] estimators,
+                                           object X, double scale,
+                                           np.ndarray[float64, ndim=2] out):
+    """Predicts output for regression tree inplace and adds scaled value to ``out[i, k]``.
+
+    The function assumes that the ndarray that wraps ``X`` is csr_matrix.
+    """
+    cdef DTYPE_t* X_data = <DTYPE_t*>(<np.ndarray> X.data).data
+    cdef INT32_t* X_indices = <INT32_t*>(<np.ndarray> X.indices).data
+    cdef INT32_t* X_indptr = <INT32_t*>(<np.ndarray> X.indptr).data
+
+    cdef SIZE_t n_samples = X.shape[0]
+    cdef SIZE_t n_features = X.shape[1]
+    cdef SIZE_t n_stages = estimators.shape[0]
+    cdef SIZE_t n_outputs = estimators.shape[1]
+
+    # Initialize output
+    cdef float64* out_ptr = <float64*> out.data
+
+    # Indices and temporary variables
+    cdef SIZE_t sample_i
+    cdef SIZE_t feature_i
+    cdef SIZE_t stage_i
+    cdef SIZE_t output_i
+    cdef Node *root_node = NULL
+    cdef Node *node = NULL
+    cdef double *value = NULL
+
+    cdef Tree tree
+    cdef Node** nodes = NULL
+    cdef double** values = NULL
+    safe_realloc(&nodes, n_stages * n_outputs)
+    safe_realloc(&values, n_stages * n_outputs)
+    for stage_i in range(n_stages):
+        for output_i in range(n_outputs):
+            tree = estimators[stage_i, output_i].tree_
+            nodes[stage_i * n_outputs + output_i] = tree.nodes
+            values[stage_i * n_outputs + output_i] = tree.value
+
+    # Initialize auxiliary data-structure
+    cdef DTYPE_t feature_value = 0.
+    cdef DTYPE_t* X_sample = NULL
+
+    # feature_to_sample as a data structure records the last seen sample
+    # for each feature; functionally, it is an efficient way to identify
+    # which features are nonzero in the present sample.
+    cdef SIZE_t* feature_to_sample = NULL
+
+    safe_realloc(&X_sample, n_features)
+    safe_realloc(&feature_to_sample, n_features)
+
+    memset(feature_to_sample, -1, n_features * sizeof(SIZE_t))
+
+    # Cycle through all samples
+    for sample_i in range(n_samples):
+        for feature_i in range(X_indptr[sample_i], X_indptr[sample_i + 1]):
+            feature_to_sample[X_indices[feature_i]] = sample_i
+            X_sample[X_indices[feature_i]] = X_data[feature_i]
+
+        # Cycle through all stages
+        for stage_i in range(n_stages):
+            # Cycle through all trees
+            for output_i in range(n_outputs):
+                root_node = nodes[stage_i * n_outputs + output_i]
+                value = values[stage_i * n_outputs + output_i]
+                node = root_node
+
+                # While node not a leaf
+                while node.left_child != TREE_LEAF:
+                    # ... and node.right_child != TREE_LEAF:
+                    if feature_to_sample[node.feature] == sample_i:
+                        feature_value = X_sample[node.feature]
+                    else:
+                        feature_value = 0.
+
+                    if feature_value <= node.threshold:
+                        node = root_node + node.left_child
+                    else:
+                        node = root_node + node.right_child
+                out_ptr[sample_i * n_outputs + output_i] += (scale
+                    * value[node - root_node])
+
+    # Free auxiliary arrays
+    free(X_sample)
+    free(feature_to_sample)
+    free(nodes)
+    free(values)
+

-@cython.nonecheck(False)
 def predict_stages(np.ndarray[object, ndim=2] estimators,
-                   np.ndarray[DTYPE_t, ndim=2, mode='c'] X, double scale,
+                   object X, double scale,
                    np.ndarray[float64, ndim=2] out):
     """Add predictions of ``estimators`` to ``out``.

@@ -112,25 +202,31 @@ def predict_stages(np.ndarray[object, ndim=2] estimators,
     cdef Py_ssize_t K = estimators.shape[1]
     cdef Tree tree

-    for i in range(n_estimators):
-        for k in range(K):
-            tree = estimators[i, k].tree_
+    if issparse(X):
+        _predict_regression_tree_stages_sparse(estimators, X, scale, out)
+    else:
+        if not isinstance(X, np.ndarray):
+            raise ValueError("X should be in np.ndarray or csr_matrix format,"
+                             "got %s" % type(X))
+
+        for i in range(n_estimators):
+            for k in range(K):
+                tree = estimators[i, k].tree_

-            # avoid buffer validation by casting to ndarray
-            # and get data pointer
-            # need brackets because of casting operator priority
-            _predict_regression_tree_inplace_fast(
-                <DTYPE_t*> X.data,
-                tree.nodes, tree.value,
-                scale, k, K, X.shape[0], X.shape[1],
-                <float64 *> (<np.ndarray> out).data)
-            ## out += scale * tree.predict(X).reshape((X.shape[0], 1))
+                # avoid buffer validation by casting to ndarray
+                # and get data pointer
+                # need brackets because of casting operator priority
+                _predict_regression_tree_inplace_fast_dense(
+                    <DTYPE_t*> (<np.ndarray> X).data,
+                    tree.nodes, tree.value,
+                    scale, k, K, X.shape[0], X.shape[1],
+                    <float64 *> (<np.ndarray> out).data)
+                ## out += scale * tree.predict(X).reshape((X.shape[0], 1))

-@cython.nonecheck(False)
 def predict_stage(np.ndarray[object, ndim=2] estimators,
                   int stage,
-                  np.ndarray[DTYPE_t, ndim=2] X, double scale,
+                  object X, double scale,
                   np.ndarray[float64, ndim=2] out):
     """Add predictions of ``estimators[stage]`` to ``out``.

@@ -216,7 +312,7 @@ cpdef _partial_dependence_tree(Tree tree, DTYPE_t[:, ::1] X,
             stack_size -= 1
             current_node = node_stack[stack_size]

-            if current_node.left_child == LEAF:
+            if current_node.left_child == TREE_LEAF:
                 out[i] += weight_stack[stack_size] * value[current_node - root_node] * \
                           learn_rate
                 total_weight += weight_stack[stack_size]
diff --git a/sklearn/ensemble/gradient_boosting.py b/sklearn/ensemble/gradient_boosting.py
index c9a36aac1bd99..fcfeb45a09157 100644
--- a/sklearn/ensemble/gradient_boosting.py
+++ b/sklearn/ensemble/gradient_boosting.py
@@ -1158,8 +1158,10 @@ def _staged_decision_function(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
@@ -1169,7 +1171,7 @@ def _staged_decision_function(self, X):
             Regression and binary classification are special cases with
             ``k == 1``, otherwise ``k==n_classes``.
         """
-        X = check_array(X, dtype=DTYPE, order="C")
+        X = check_array(X, dtype=DTYPE, order="C",  accept_sparse='csr')
         score = self._init_decision_function(X)
         for i in range(self.estimators_.shape[0]):
             predict_stage(self.estimators_, i, X, self.learning_rate, score)
@@ -1479,8 +1481,10 @@ def decision_function(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
@@ -1490,7 +1494,7 @@ def decision_function(self, X):
             Regression and binary classification produce an array of shape
             [n_samples].
         """
-        X = check_array(X, dtype=DTYPE, order="C")
+        X = check_array(X, dtype=DTYPE, order="C",  accept_sparse='csr')
         score = self._decision_function(X)
         if score.shape[1] == 1:
             return score.ravel()
@@ -1504,8 +1508,10 @@ def staged_decision_function(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
@@ -1524,8 +1530,10 @@ def predict(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
@@ -1544,8 +1552,10 @@ def staged_predict(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
@@ -1561,8 +1571,10 @@ def predict_proba(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Raises
         ------
@@ -1589,8 +1601,10 @@ def predict_log_proba(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Raises
         ------
@@ -1614,8 +1628,10 @@ def staged_predict_proba(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
@@ -1840,15 +1856,17 @@ def predict(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
         y : array of shape = [n_samples]
             The predicted values.
         """
-        X = check_array(X, dtype=DTYPE, order="C")
+        X = check_array(X, dtype=DTYPE, order="C",  accept_sparse='csr')
         return self._decision_function(X).ravel()

     def staged_predict(self, X):
@@ -1859,8 +1877,10 @@ def staged_predict(self, X):

         Parameters
         ----------
-        X : array-like of shape = [n_samples, n_features]
-            The input samples.
+        X : array-like or sparse matrix, shape = [n_samples, n_features]
+            The input samples. Internally, it will be converted to
+            ``dtype=np.float32`` and if a sparse matrix is provided
+            to a sparse ``csr_matrix``.

         Returns
         -------
diff --git a/sklearn/tree/_tree.pyx b/sklearn/tree/_tree.pyx
index 4e8160f7dc780..4f11cff0569e6 100644
--- a/sklearn/tree/_tree.pyx
+++ b/sklearn/tree/_tree.pyx
@@ -839,8 +839,8 @@ cdef class Tree:
         # which features are nonzero in the present sample.
         cdef SIZE_t* feature_to_sample = NULL

-        safe_realloc(&X_sample, n_features * sizeof(DTYPE_t))
-        safe_realloc(&feature_to_sample, n_features * sizeof(SIZE_t))
+        safe_realloc(&X_sample, n_features)
+        safe_realloc(&feature_to_sample, n_features)

         with nogil:
             memset(feature_to_sample, -1, n_features * sizeof(SIZE_t))
@@ -985,8 +985,8 @@ cdef class Tree:
         # which features are nonzero in the present sample.
         cdef SIZE_t* feature_to_sample = NULL

-        safe_realloc(&X_sample, n_features * sizeof(DTYPE_t))
-        safe_realloc(&feature_to_sample, n_features * sizeof(SIZE_t))
+        safe_realloc(&X_sample, n_features)
+        safe_realloc(&feature_to_sample, n_features)

         with nogil:
             memset(feature_to_sample, -1, n_features * sizeof(SIZE_t))
diff --git a/sklearn/tree/_utils.pxd b/sklearn/tree/_utils.pxd
index d11880908c318..fce3abcb734db 100644
--- a/sklearn/tree/_utils.pxd
+++ b/sklearn/tree/_utils.pxd
@@ -10,6 +10,7 @@

 import numpy as np
 cimport numpy as np
+from _tree cimport Node

 ctypedef np.npy_float32 DTYPE_t          # Type of X
 ctypedef np.npy_float64 DOUBLE_t         # Type of y, sample_weight
@@ -36,6 +37,9 @@ ctypedef fused realloc_ptr:
     (unsigned char*)
     (WeightedPQueueRecord*)
     (DOUBLE_t*)
+    (DOUBLE_t**)
+    (Node*)
+    (Node**)

 cdef realloc_ptr safe_realloc(realloc_ptr* p, size_t nelems) except *

diff --git a/sklearn/ensemble/tests/test_gradient_boosting.py b/sklearn/ensemble/tests/test_gradient_boosting.py
index 1ebf82fa3ae44..634bc259a1167 100644
--- a/sklearn/ensemble/tests/test_gradient_boosting.py
+++ b/sklearn/ensemble/tests/test_gradient_boosting.py
@@ -1050,6 +1050,9 @@ def check_sparse_input(EstimatorClass, X, X_sparse, y):
     assert_array_almost_equal(sparse.feature_importances_,
                               auto.feature_importances_)

+    assert_array_almost_equal(sparse.predict(X_sparse), dense.predict(X))
+    assert_array_almost_equal(dense.predict(X_sparse), sparse.predict(X))
+
     if isinstance(EstimatorClass, GradientBoostingClassifier):
         assert_array_almost_equal(sparse.predict_proba(X),
                                   dense.predict_proba(X))
@@ -1061,6 +1064,14 @@ def check_sparse_input(EstimatorClass, X, X_sparse, y):
         assert_array_almost_equal(sparse.predict_log_proba(X),
                                   auto.predict_log_proba(X))

+        assert_array_almost_equal(sparse.decision_function(X_sparse),
+                                  sparse.decision_function(X))
+        assert_array_almost_equal(dense.decision_function(X_sparse),
+                                  sparse.decision_function(X))
+
+        assert_array_almost_equal(
+            np.array(sparse.staged_decision_function(X_sparse)),
+            np.array(sparse.staged_decision_function(X)))

 @skip_if_32bit
 def test_sparse_input():

GradientBoostingClassifier.fit accepts sparse X, but .predict does not
I have a sparse dataset that is too large for main memory if I call `X.todense()`.  If I understand correctly, `GradientBoostingClassifier.fit` will accept my sparse `X`, but it is not currently possible to use `GradientBoostingClassifier.predict` on the results.  It would be great if that were not the case.

Here is a minimal example of the issue:

```
from scipy import sparse
from sklearn.datasets.samples_generator import make_classification
from sklearn.ensemble import GradientBoostingClassifier

X, y = make_classification(n_samples=20, n_features=5, random_state=0)
X_sp = sparse.coo_matrix(X)

clf = GradientBoostingClassifier()
clf.fit(X,y)
clf.predict(X)  # works

clf.fit(X_sp, y)  # works
clf.predict(X_sp)  # fails with TypeError: A sparse matrix was passed, but dense data is required.
```

Confirmed. I think small rework of https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/ensemble/_gradient_boosting.pyx#L39 is needed.
Looks like function _predict_regression_tree_inplace_fast was written to somehow optimize prediction speed. But i see some problems in it, its main loop https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/ensemble/_gradient_boosting.pyx#L92 differs from the main loop here https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/tree/_tree.pyx#L784, and additionally this function doesn't  work with sparse matrices.

Maybe it's better to rely on predict method of Tree class, but some performance tests is needed.


diff --git a/.gitignore b/.gitignore
index c5586c4..cc6983a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,8 @@ docs/_build/
 .vscode
 db.sqlite
 **/__pycache__/
+**/latest_logs
+**/archived_logs

 # Coverage reports
 htmlcov/
diff --git a/app/models.py b/app/models.py
index 5ae4e11..562268e 100644
--- a/app/models.py
+++ b/app/models.py
@@ -142,6 +142,10 @@ def register(name, email, real_name, password):
       Returns:
         True if registration succeeded otherwise False
     '''
+    # Check that the email has to follow addr-spec defined in RFC 5322
+    if not email_check(email):
+        return False
+
     # check if the email has been used:
     existed = User.query.filter_by(email=email).all()
     if len(existed) > 0:
@@ -151,10 +155,6 @@ def register(name, email, real_name, password):
     if not not_empty(email) and not not_empty(password):
         return False

-    # Check that the email has to follow addr-spec defined in RFC 5322
-    if not email_check(email):
-        return False
-
     # Check that the password has to meet the required complexity:
     # minimum length 6, at least one upper case, at least one lower
     # case, and at least one special character.

diff --git a/app_test/frontend/test_listing.py b/app_test/frontend/test_listing.py
index bc47c57..c6a75da 100644
--- a/app_test/frontend/test_listing.py
+++ b/app_test/frontend/test_listing.py
@@ -1,5 +1,4 @@
 from seleniumbase import BaseCase
-
 from app_test.conftest import base_url
 from app.models import Booking, Listing, User
 from app.models import db
@@ -425,7 +424,7 @@ def test_listing_helper(self, *_):
         self.open(base_url + "/register")
         # fill details
         self.type("#email", "test2@test.com")
-        self.type("#real_name", "real username")
+        self.type("#real_name", "Test Test")
         self.type("#name", "Test")
         self.type("#password", "Test!123")
         self.type("#password2", "Test!123")
diff --git a/app_test/frontend/test_login.py b/app_test/frontend/test_login.py
index 51a0023..e964bfa 100644
--- a/app_test/frontend/test_login.py
+++ b/app_test/frontend/test_login.py
@@ -1,5 +1,4 @@
 from seleniumbase import BaseCase
-
 from app_test.conftest import base_url
 from app.models import User
 from app.models import db
diff --git a/app_test/frontend/test_user_update.py b/app_test/frontend/test_user_update.py
index 5451461..48c040e 100644
--- a/app_test/frontend/test_user_update.py
+++ b/app_test/frontend/test_user_update.py
@@ -1,5 +1,4 @@
 from seleniumbase import BaseCase
-
 from app_test.conftest import base_url
 from app.models import User
 from app.models import db
diff --git a/app_test/injection_tests.py b/app_test/injection_tests.py
index 599299e..aad70bc 100644
--- a/app_test/injection_tests.py
+++ b/app_test/injection_tests.py
@@ -6,16 +6,35 @@

 def test_sqli_register():
     '''
-    Function to test SQL Injection handling for Register method
+    Function to test SQL Injection handling for registration
     '''
     injection_file = open('app_test/Generic_SQLI.txt', 'r')
     lines = injection_file.readlines()

     for line in lines:
+        test_name_parameter(line)
+        test_email_parameter(line)
         real_name_register(line)
         password_register(line)

+def test_name_parameter(line):
+    '''
+    Function to test SQL Injection handling for registration
+    parameter 'name'.
+    '''
+    assert register(line, 'realname@gmail.com',
+                    'real name', '12345Aa#') is False
+
+
+def test_email_parameter(line):
+    '''
+    Function to test SQL Injection handling for registration
+    parameter 'name'.
+    '''
+    assert register('user50', line, 'real name', '12345Aa#') is False
+
+
 def test_sqli_create_listing():
     '''
     Function to test SQL Injection handling for Create Listing methods

Test SQL Injection: Register Function (name, email)